aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorÖzgür Kesim <oec-taler@kesim.org>2023-10-06 16:33:05 +0200
committerÖzgür Kesim <oec-taler@kesim.org>2023-10-06 16:33:05 +0200
commitfe7b51ef2736edbf04f5bbd9d19f2a2d04baccc2 (patch)
tree66c68c8d6a666f6e74dc663c9ee4f07879f6626c /packages
parent35611f0bf9cf67638b171c2a300fab1797d3d8f0 (diff)
parent97d7be7503168f4f3bbd05905d32aa76ca1636b2 (diff)
Merge branch 'master' into age-withdraw
Diffstat (limited to 'packages')
-rw-r--r--packages/aml-backoffice-ui/Makefile3
-rw-r--r--packages/anastasis-cli/Makefile21
-rw-r--r--packages/demobank-ui/Makefile3
-rw-r--r--packages/demobank-ui/README.md9
-rwxr-xr-xpackages/demobank-ui/build.mjs4
-rwxr-xr-xpackages/demobank-ui/dev.mjs6
-rw-r--r--packages/demobank-ui/package.json6
-rw-r--r--packages/demobank-ui/postcss.config.js6
-rw-r--r--packages/demobank-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/demobank-ui/src/components/Attention.tsx59
-rw-r--r--packages/demobank-ui/src/components/Cashouts/views.tsx5
-rw-r--r--packages/demobank-ui/src/components/CopyButton.tsx60
-rw-r--r--packages/demobank-ui/src/components/ErrorLoading.tsx (renamed from packages/demobank-ui/src/scss/_misc.scss)47
-rw-r--r--packages/demobank-ui/src/components/LangSelector.tsx78
-rw-r--r--packages/demobank-ui/src/components/QR.tsx5
-rw-r--r--packages/demobank-ui/src/components/Routing.tsx167
-rw-r--r--packages/demobank-ui/src/components/ShowInputErrorLabel.tsx (renamed from packages/demobank-ui/src/pages/ShowInputErrorLabel.tsx)4
-rw-r--r--packages/demobank-ui/src/components/Transactions/index.ts2
-rw-r--r--packages/demobank-ui/src/components/Transactions/state.ts52
-rw-r--r--packages/demobank-ui/src/components/Transactions/views.tsx139
-rw-r--r--packages/demobank-ui/src/components/app.tsx59
-rw-r--r--packages/demobank-ui/src/context/backend.ts4
-rw-r--r--packages/demobank-ui/src/context/config.ts (renamed from packages/demobank-ui/src/scss/_title-bar.scss)62
-rw-r--r--packages/demobank-ui/src/declaration.d.ts213
-rw-r--r--packages/demobank-ui/src/demobank-ui-settings.js21
-rw-r--r--packages/demobank-ui/src/forms/simplest.ts66
-rw-r--r--packages/demobank-ui/src/hooks/access.ts131
-rw-r--r--packages/demobank-ui/src/hooks/backend.ts113
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts29
-rw-r--r--packages/demobank-ui/src/hooks/config.ts59
-rw-r--r--packages/demobank-ui/src/hooks/notification.ts54
-rw-r--r--packages/demobank-ui/src/hooks/settings.ts22
-rw-r--r--packages/demobank-ui/src/hooks/useCredentialsChecker.ts135
-rw-r--r--packages/demobank-ui/src/index.html49
-rw-r--r--packages/demobank-ui/src/index.tsx2
-rw-r--r--packages/demobank-ui/src/pages/AccountPage.tsx170
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/index.ts92
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/state.ts92
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/stories.tsx (renamed from packages/demobank-ui/src/scss/_footer.scss)22
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/test.ts (renamed from packages/demobank-ui/src/scss/_mixins.scss)24
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/views.tsx93
-rw-r--r--packages/demobank-ui/src/pages/AdminPage.tsx1064
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx609
-rw-r--r--packages/demobank-ui/src/pages/HomePage.tsx96
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx360
-rw-r--r--packages/demobank-ui/src/pages/OperationState/index.ts122
-rw-r--r--packages/demobank-ui/src/pages/OperationState/state.ts265
-rw-r--r--packages/demobank-ui/src/pages/OperationState/stories.tsx (renamed from packages/demobank-ui/src/scss/_tiles.scss)13
-rw-r--r--packages/demobank-ui/src/pages/OperationState/test.ts (renamed from packages/demobank-ui/src/scss/_modal.scss)25
-rw-r--r--packages/demobank-ui/src/pages/OperationState/views.tsx376
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx134
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx640
-rw-r--r--packages/demobank-ui/src/pages/PublicHistoriesPage.tsx16
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.tsx126
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx440
-rw-r--r--packages/demobank-ui/src/pages/Routing.tsx110
-rw-r--r--packages/demobank-ui/src/pages/ShowAccountDetails.tsx167
-rw-r--r--packages/demobank-ui/src/pages/UpdateAccountPassword.tsx177
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx339
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx389
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx119
-rw-r--r--packages/demobank-ui/src/pages/admin/Account.tsx38
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx315
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountList.tsx132
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx101
-rw-r--r--packages/demobank-ui/src/pages/admin/Home.tsx148
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx171
-rw-r--r--packages/demobank-ui/src/pages/business/Home.tsx (renamed from packages/demobank-ui/src/pages/BusinessAccount.tsx)226
-rw-r--r--packages/demobank-ui/src/pages/rnd.ts2895
-rw-r--r--packages/demobank-ui/src/scss/DurationPicker.scss70
-rw-r--r--packages/demobank-ui/src/scss/_aside.scss128
-rw-r--r--packages/demobank-ui/src/scss/_card.scss69
-rw-r--r--packages/demobank-ui/src/scss/_custom-calendar.scss263
-rw-r--r--packages/demobank-ui/src/scss/_form.scss71
-rw-r--r--packages/demobank-ui/src/scss/_hero-bar.scss55
-rw-r--r--packages/demobank-ui/src/scss/_loading.scss51
-rw-r--r--packages/demobank-ui/src/scss/_main-section.scss24
-rw-r--r--packages/demobank-ui/src/scss/_nav-bar.scss144
-rw-r--r--packages/demobank-ui/src/scss/_table.scss179
-rw-r--r--packages/demobank-ui/src/scss/_theme-default.scss136
-rw-r--r--packages/demobank-ui/src/scss/bank.scss353
-rw-r--r--packages/demobank-ui/src/scss/colors-bank.scss31
-rw-r--r--packages/demobank-ui/src/scss/demo.scss167
-rw-r--r--packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttfbin43752 -> 0 bytes
-rw-r--r--packages/demobank-ui/src/scss/fonts/nunito.css22
-rw-r--r--packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eotbin844600 -> 0 bytes
-rw-r--r--packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttfbin844380 -> 0 bytes
-rw-r--r--packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woffbin404384 -> 0 bytes
-rw-r--r--packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2bin283040 -> 0 bytes
-rw-r--r--packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css15109
-rw-r--r--packages/demobank-ui/src/scss/libs/_all.scss29
-rw-r--r--packages/demobank-ui/src/scss/main.css3
-rw-r--r--packages/demobank-ui/src/scss/main.scss5
-rw-r--r--packages/demobank-ui/src/scss/pure.scss1397
-rw-r--r--packages/demobank-ui/src/scss/toggle.scss51
-rw-r--r--packages/demobank-ui/src/settings.ts16
-rw-r--r--packages/demobank-ui/src/stories.test.ts3
-rw-r--r--packages/demobank-ui/src/stories.tsx2
-rw-r--r--packages/demobank-ui/src/utils.ts40
-rw-r--r--packages/demobank-ui/tailwind.config.js8
-rw-r--r--packages/merchant-backoffice-ui/Makefile3
-rw-r--r--packages/merchant-backoffice-ui/src/Application.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/InstanceRoutes.tsx30
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputStock.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx29
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/index.tsx8
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/context/backend.test.ts4
-rw-r--r--packages/merchant-backoffice-ui/src/context/backend.ts26
-rw-r--r--packages/merchant-backoffice-ui/src/declaration.d.ts4
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/backend.ts2
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/index.ts3
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.test.ts4
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.ts20
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/otp.ts4
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/product.ts17
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx13
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx52
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx27
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx77
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx37
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx17
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/login/index.tsx17
-rw-r--r--packages/merchant-backoffice-ui/src/schemas/index.ts3
-rw-r--r--packages/merchant-backoffice-ui/src/utils/constants.ts2
-rw-r--r--packages/taler-harness/Makefile22
-rw-r--r--packages/taler-harness/package.json4
-rw-r--r--packages/taler-harness/src/bench1.ts6
-rw-r--r--packages/taler-harness/src/bench3.ts6
-rw-r--r--packages/taler-harness/src/env-full.ts6
-rw-r--r--packages/taler-harness/src/harness/harness.ts181
-rw-r--r--packages/taler-harness/src/harness/helpers.ts22
-rw-r--r--packages/taler-harness/src/harness/libeufin-apis.ts775
-rw-r--r--packages/taler-harness/src/harness/libeufin.ts1047
-rw-r--r--packages/taler-harness/src/index.ts14
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-bank-api.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-deposit.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-deposit.ts2
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-management.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-purse.ts2
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-fee-regression.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-forced-selection.ts2
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc.ts14
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts108
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts56
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts68
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts70
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts65
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts76
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts69
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts106
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts63
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-bad-gateway.ts75
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-bank.ts222
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-basic.ts317
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts147
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts179
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-keyrotation.ts82
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts117
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-refund-multiple-users.ts104
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-refund.ts101
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts85
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-tutorial.ts130
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances.ts8
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-fault.ts12
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-multiple.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-revocation.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-tipping.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-dbless.ts2
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-gendb.ts110
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-notifications.ts14
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallettesting.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts16
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts11
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts6
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts42
-rw-r--r--packages/taler-util/package.json4
-rw-r--r--packages/taler-util/src/MerchantApiClient.ts2
-rw-r--r--packages/taler-util/src/amounts.ts5
-rw-r--r--packages/taler-util/src/bank-api-client.ts173
-rw-r--r--packages/taler-util/src/errors.ts2
-rw-r--r--packages/taler-util/src/payto.ts11
-rw-r--r--packages/taler-util/src/taler-types.ts13
-rw-r--r--packages/taler-util/src/talerconfig.ts6
-rw-r--r--packages/taler-util/src/time.ts6
-rw-r--r--packages/taler-util/src/transactions-types.ts24
-rw-r--r--packages/taler-util/src/wallet-types.ts44
-rw-r--r--packages/taler-wallet-cli/Makefile27
-rw-r--r--packages/taler-wallet-cli/package.json4
-rw-r--r--packages/taler-wallet-cli/src/index.ts67
-rw-r--r--packages/taler-wallet-core/package.json4
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts22
-rw-r--r--packages/taler-wallet-core/src/db.ts263
-rw-r--r--packages/taler-wallet-core/src/dbless.ts17
-rw-r--r--packages/taler-wallet-core/src/host-impl.node.ts5
-rw-r--r--packages/taler-wallet-core/src/operations/attention.ts7
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts31
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts47
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts17
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts67
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts37
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts29
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts46
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts16
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts112
-rw-r--r--packages/taler-wallet-core/src/operations/recoup.ts5
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts25
-rw-r--r--packages/taler-wallet-core/src/operations/reward.ts23
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts194
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts52
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.test.ts114
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts46
-rw-r--r--packages/taler-wallet-core/src/pending-types.ts14
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.test.ts244
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts25
-rw-r--r--packages/taler-wallet-core/src/util/denominations.ts9
-rw-r--r--packages/taler-wallet-core/src/util/instructedAmountConversion.ts17
-rw-r--r--packages/taler-wallet-core/src/versions.ts2
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts45
-rw-r--r--packages/taler-wallet-core/src/wallet.ts79
-rw-r--r--packages/taler-wallet-embedded/package.json4
-rw-r--r--packages/taler-wallet-embedded/src/wallet-qjs.ts6
-rw-r--r--packages/taler-wallet-webextension/manifest-common.json4
-rw-r--r--packages/taler-wallet-webextension/package.json4
-rw-r--r--packages/taler-wallet-webextension/src/components/BalanceTable.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/components/HistoryItem.tsx30
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/popup/BalancePage.tsx16
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.tsx12
-rw-r--r--packages/web-util/package.json4
-rw-r--r--packages/web-util/src/forms/Caption.tsx32
-rw-r--r--packages/web-util/src/forms/DefaultForm.tsx65
-rw-r--r--packages/web-util/src/forms/FormProvider.tsx99
-rw-r--r--packages/web-util/src/forms/Group.tsx41
-rw-r--r--packages/web-util/src/forms/InputAmount.tsx34
-rw-r--r--packages/web-util/src/forms/InputArray.tsx183
-rw-r--r--packages/web-util/src/forms/InputChoiceHorizontal.tsx82
-rw-r--r--packages/web-util/src/forms/InputChoiceStacked.tsx111
-rw-r--r--packages/web-util/src/forms/InputDate.tsx37
-rw-r--r--packages/web-util/src/forms/InputFile.tsx101
-rw-r--r--packages/web-util/src/forms/InputInteger.tsx23
-rw-r--r--packages/web-util/src/forms/InputLine.tsx282
-rw-r--r--packages/web-util/src/forms/InputSelectMultiple.tsx151
-rw-r--r--packages/web-util/src/forms/InputSelectOne.tsx134
-rw-r--r--packages/web-util/src/forms/InputText.tsx8
-rw-r--r--packages/web-util/src/forms/InputTextArea.tsx8
-rw-r--r--packages/web-util/src/forms/forms.ts135
-rw-r--r--packages/web-util/src/forms/index.ts19
-rw-r--r--packages/web-util/src/forms/useField.ts93
-rw-r--r--packages/web-util/src/hooks/index.ts6
-rw-r--r--packages/web-util/src/hooks/useNotifications.ts44
-rw-r--r--packages/web-util/src/index.browser.ts1
-rw-r--r--packages/web-util/src/index.build.ts2
-rw-r--r--packages/web-util/src/utils/request.ts2
271 files changed, 12415 insertions, 26719 deletions
diff --git a/packages/aml-backoffice-ui/Makefile b/packages/aml-backoffice-ui/Makefile
index 2653ce92f..64f9f83d1 100644
--- a/packages/aml-backoffice-ui/Makefile
+++ b/packages/aml-backoffice-ui/Makefile
@@ -3,6 +3,7 @@
ifeq ($(TOPLEVEL), yes)
$(info top-level build)
-include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
else
$(info package-level build)
-include ../../.config.mk
@@ -15,7 +16,7 @@ $(info prefix is $(prefix))
all:
@echo run \'make install\' to install
-spa_dir=$(prefix)/share/taler/aml-backoffice-ui
+spa_dir=$(DESTDIR)$(prefix)/share/taler/aml-backoffice-ui
.PHONY: install-nodeps
install-nodeps:
diff --git a/packages/anastasis-cli/Makefile b/packages/anastasis-cli/Makefile
index 292f7000f..724a5e40d 100644
--- a/packages/anastasis-cli/Makefile
+++ b/packages/anastasis-cli/Makefile
@@ -3,6 +3,7 @@
ifeq ($(TOPLEVEL), yes)
$(info top-level build)
-include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
else
$(info package-level build)
-include ../../.config.mk
@@ -20,19 +21,19 @@ warn-noprefix:
@echo "no prefix configured, did you run ./configure?"
install: warn-noprefix
else
-install_target = $(prefix)/lib/anastasis-cli
+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 $(prefix)/bin
- install -d $(install_target)/bin
- install -d $(install_target)/node_modules/anastasis-cli
- install -d $(install_target)/node_modules/anastasis-cli/bin
- install -d $(install_target)/node_modules/anastasis-cli/dist
- install ./dist/anastasis-cli-bundled.cjs $(install_target)/node_modules/anastasis-cli/dist/
- install ./dist/anastasis-cli-bundled.cjs.map $(install_target)/node_modules/anastasis-cli/dist/
- install ./bin/anastasis-cli.mjs $(install_target)/node_modules/anastasis-cli/bin/
- ln -sf $(install_target)/node_modules/anastasis-cli/bin/anastasis-cli.mjs $(prefix)/bin/anastasis-cli
+ 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:
diff --git a/packages/demobank-ui/Makefile b/packages/demobank-ui/Makefile
index 8e41cc7c6..2399cc427 100644
--- a/packages/demobank-ui/Makefile
+++ b/packages/demobank-ui/Makefile
@@ -3,6 +3,7 @@
ifeq ($(TOPLEVEL), yes)
$(info top-level build)
-include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
else
$(info package-level build)
-include ../../.config.mk
@@ -15,7 +16,7 @@ $(info prefix is $(prefix))
all:
@echo run \'make install\' to install
-spa_dir=$(prefix)/share/taler/demobank-ui
+spa_dir=$(DESTDIR)$(prefix)/share/taler/demobank-ui
.PHONY: deps
deps:
diff --git a/packages/demobank-ui/README.md b/packages/demobank-ui/README.md
index 1732b5f38..877799748 100644
--- a/packages/demobank-ui/README.md
+++ b/packages/demobank-ui/README.md
@@ -36,11 +36,18 @@ to the default settings:
```
globalThis.talerDemobankSettings = {
- backendBaseURL: "https://bank.demo.taler.net/demobanks/default/",
+ // location of libeufin server
+ backendBaseURL: "https://bank.demo.taler.net/",
allowRegistrations: true,
bankName: "Taler Bank",
// Show explainer text and navbar to other demo sites
showDemoNav: true,
+ // href value of the icon in the top left
+ iconLinkURL: "https://demo.taler.net/",
+ // show the button "create random user" in registration form
+ allowRandomAccountCreation: true,
+ // do not create random password for random users
+ simplePasswordForRandomAccounts: true,
// Names and links for other demo sites to show in the navbar
demoSites: [
["Landing", "https://demo.taler.net/"],
diff --git a/packages/demobank-ui/build.mjs b/packages/demobank-ui/build.mjs
index 22b91803a..64ddc3774 100755
--- a/packages/demobank-ui/build.mjs
+++ b/packages/demobank-ui/build.mjs
@@ -21,8 +21,8 @@ await build({
type: "production",
source: {
js: ["src/index.tsx"],
- assets: [{base:"src",files:["src/index.html"]}],
+ assets: [{ base: "src", files: ["src/index.html"] }],
},
destination: "./dist/prod",
- css: "sass",
+ css: "postcss",
});
diff --git a/packages/demobank-ui/dev.mjs b/packages/demobank-ui/dev.mjs
index 8b870451b..f29a05e49 100755
--- a/packages/demobank-ui/dev.mjs
+++ b/packages/demobank-ui/dev.mjs
@@ -18,17 +18,17 @@
import { serve } from "@gnu-taler/web-util/node";
import { initializeDev } from "@gnu-taler/web-util/build";
-const devEntryPoints = ["src/stories.tsx", "src/index.tsx"];
+const devEntryPoints = ["src/stories.tsx", "src/index.tsx", "src/demobank-ui-settings.js"];
const build = initializeDev({
type: "development",
source: {
js: devEntryPoints,
- assets: [{base:"src",files:["src/index.html"]}],
+ assets: [{ base: "src", files: ["src/index.html"] }],
},
destination: "./dist/dev",
public: "/app",
- css: "sass",
+ css: "postcss",
});
await build();
diff --git a/packages/demobank-ui/package.json b/packages/demobank-ui/package.json
index 8b999aeed..17059afeb 100644
--- a/packages/demobank-ui/package.json
+++ b/packages/demobank-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/demobank-ui",
- "version": "0.1.0",
+ "version": "0.9.3-dev.27",
"license": "AGPL-3.0-OR-LATER",
"type": "module",
"scripts": {
@@ -46,6 +46,9 @@
"devDependencies": {
"@creativebulma/bulma-tooltip": "^1.2.0",
"@gnu-taler/pogen": "^0.0.5",
+ "@tailwindcss/forms": "^0.5.3",
+ "@tailwindcss/typography": "^0.5.9",
+ "autoprefixer": "^10.4.14",
"@types/chai": "^4.3.0",
"@types/history": "^4.7.8",
"@types/mocha": "^10.0.1",
@@ -62,6 +65,7 @@
"po2json": "^0.4.5",
"preact-render-to-string": "^5.2.6",
"sass": "1.56.1",
+ "tailwindcss": "^3.3.2",
"typescript": "5.2.2"
},
"pogen": {
diff --git a/packages/demobank-ui/postcss.config.js b/packages/demobank-ui/postcss.config.js
new file mode 100644
index 000000000..2e7af2b7f
--- /dev/null
+++ b/packages/demobank-ui/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/packages/demobank-ui/src/assets/logo-2021.svg b/packages/demobank-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/demobank-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/components/Attention.tsx b/packages/demobank-ui/src/components/Attention.tsx
new file mode 100644
index 000000000..3313e5796
--- /dev/null
+++ b/packages/demobank-ui/src/components/Attention.tsx
@@ -0,0 +1,59 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { assertUnreachable } from "./Routing.js";
+
+interface Props {
+ type?: "info" | "success" | "warning" | "danger",
+ onClose?: () => void,
+ title: TranslatedString,
+ children?: ComponentChildren ,
+}
+export function Attention({ type = "info", title, children, onClose }: Props): VNode {
+ return <div class={`group attention-${type} mt-2`}>
+ <div class="rounded-md group-[.attention-info]:bg-blue-50 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 >
+ <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 group-hover:text-white 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>
+
+ </div>
+}
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx
index 4b7649fb6..a32deb266 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -19,6 +19,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { State } from "./index.js";
import { format } from "date-fns";
import { Amounts } from "@gnu-taler/taler-util";
+import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
@@ -62,8 +63,8 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
? format(item.confirmation_time, "dd/MM/yyyy HH:mm:ss")
: "-"}
</td>
- <td>{Amounts.stringifyValue(item.amount_debit)}</td>
- <td>{Amounts.stringifyValue(item.amount_credit)}</td>
+ <td><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} /></td>
+ <td><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} /></td>
<td>{item.status}</td>
<td>
<a
diff --git a/packages/demobank-ui/src/components/CopyButton.tsx b/packages/demobank-ui/src/components/CopyButton.tsx
new file mode 100644
index 000000000..b36de770e
--- /dev/null
+++ b/packages/demobank-ui/src/components/CopyButton.tsx
@@ -0,0 +1,60 @@
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+
+
+
+export function CopyIcon(): VNode {
+ return (
+ <svg height="16" viewBox="0 0 16 16" width="16" stroke="currentColor" strokeWidth="1.5">
+ <path
+ fill-rule="evenodd"
+ d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
+ />
+ <path
+ fill-rule="evenodd"
+ d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
+ />
+ </svg>
+ )
+};
+
+export function CopiedIcon(): VNode {
+ return (
+ <svg height="16" viewBox="0 0 16 16" width="16" stroke="currentColor" strokeWidth="1.5">
+ <path
+ fill-rule="evenodd"
+ d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
+ />
+ </svg>
+ )
+};
+
+export function CopyButton({ getContent }: { getContent: () => string }): VNode {
+ const [copied, setCopied] = useState(false);
+ function copyText(): void {
+ navigator.clipboard.writeText(getContent() || "");
+ setCopied(true);
+ }
+ useEffect(() => {
+ if (copied) {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }
+ }, [copied]);
+
+ if (!copied) {
+ return (
+ <button class="text-white" onClick={copyText} style={{ width: 16, height: 16, fontSize: "initial" }}>
+ <CopyIcon />
+ </button>
+ );
+ }
+ return (
+ <div class="text-white" content="Copied" style={{ display: "inline-block" }}>
+ <button disabled style={{ width: 16, height: 16, fontSize: "initial" }}>
+ <CopiedIcon />
+ </button>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/scss/_misc.scss b/packages/demobank-ui/src/components/ErrorLoading.tsx
index 65bd28dbd..ee62671ce 100644
--- a/packages/demobank-ui/src/scss/_misc.scss
+++ b/packages/demobank-ui/src/components/ErrorLoading.tsx
@@ -1,6 +1,7 @@
/*
+/*
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,37 +15,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-.is-user-avatar {
- &.has-max-width {
- max-width: $size-base * 7;
- }
-
- &.is-aligned-center {
- margin: 0 auto;
- }
-
- img {
- margin: 0 auto;
- border-radius: $radius-rounded;
- }
-}
-
-.icon.has-update-mark {
- position: relative;
-
- &:after {
- content: "";
- width: $icon-update-mark-size;
- height: $icon-update-mark-size;
- position: absolute;
- top: 1px;
- right: 1px;
- background-color: $icon-update-mark-color;
- border-radius: $radius-rounded;
- }
+import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { Attention } from "./Attention.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+export function ErrorLoading({ error }: { error: HttpError<SandboxBackend.SandboxError> }): VNode {
+ const { i18n } = useTranslationContext()
+ return (<Attention type="danger" title={error.message as TranslatedString}>
+ <p class="text-sm font-medium text-red-800">Got status "{error.info.status}" on {error.info.url}</p>
+ </Attention>
+ );
}
diff --git a/packages/demobank-ui/src/components/LangSelector.tsx b/packages/demobank-ui/src/components/LangSelector.tsx
index ca4411682..c1d0f64ef 100644
--- a/packages/demobank-ui/src/components/LangSelector.tsx
+++ b/packages/demobank-ui/src/components/LangSelector.tsx
@@ -42,11 +42,11 @@ function getLangName(s: keyof LangsNames | string): string {
return String(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);
@@ -62,51 +62,49 @@ export function LangSelectorLikePy(): VNode {
};
}, []);
return (
- <Fragment>
- <a
- href="#"
- class="pure-button"
- name="language"
- onClick={(ev) => {
- ev.preventDefault();
- setHidden((h) => !h);
- ev.stopPropagation();
- }}
- >
- {getLangName(lang)}
- </a>
- <div
- id="lang"
- class={hidden ? "hide" : ""}
- style={{
- display: "inline-block",
- }}
- >
- <div style="position: relative; overflow: visible;">
- <div
- class="nav"
- style="position: absolute; max-height: 60vh; overflow-y: auto; margin-left: -120px; margin-top: 20px"
- >
+ <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={() => {
+ setHidden((h) => !h);
+ }}>
+ <span class="flex items-center">
+ <img src="https://taler.net/images/languageicon.svg" alt="" class="h-5 w-5 flex-shrink-0 rounded-full" />
+ <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(messages)
.filter((l) => l !== lang)
- .map((l) => (
- <a
- key={l}
- href="#"
- class="navbtn langbtn"
- value={l}
+ .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(l);
+ changeLanguage(lang);
setUpdatingLang(false);
+ setHidden(true)
}}
>
- {getLangName(l)}
- </a>
+ <span class="font-normal block truncate">{getLangName(lang)}</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>
))}
- <br />
- </div>
- </div>
+
+ </ul>
+ }
+
</div>
- </Fragment>
+ </div>
);
}
diff --git a/packages/demobank-ui/src/components/QR.tsx b/packages/demobank-ui/src/components/QR.tsx
index c1c159ef8..945a08867 100644
--- a/packages/demobank-ui/src/components/QR.tsx
+++ b/packages/demobank-ui/src/components/QR.tsx
@@ -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/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx
new file mode 100644
index 000000000..aafc95687
--- /dev/null
+++ b/packages/demobank-ui/src/components/Routing.tsx
@@ -0,0 +1,167 @@
+/*
+ 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 { createHashHistory } from "history";
+import { Fragment, VNode, h } from "preact";
+import { Route, Router, route } from "preact-router";
+import { useEffect } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { BankFrame } from "../pages/BankFrame.js";
+import { HomePage, WithdrawalOperationPage } from "../pages/HomePage.js";
+import { LoginForm } from "../pages/LoginForm.js";
+import { PublicHistoriesPage } from "../pages/PublicHistoriesPage.js";
+import { RegistrationPage } from "../pages/RegistrationPage.js";
+import { AdminHome } from "../pages/admin/Home.js";
+import { BusinessAccount } from "../pages/business/Home.js";
+import { bankUiSettings } from "../settings.js";
+
+export function Routing(): VNode {
+ const history = createHashHistory();
+ const backend = useBackendContext();
+ const {i18n} = useTranslationContext();
+
+ if (backend.state.status === "loggedOut") {
+ return <BankFrame >
+ <Router history={history}>
+ <Route
+ path="/login"
+ component={() => (
+ <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 ${bankUiSettings.bankName}!`}</h2>
+ </div>
+
+ <LoginForm
+ onRegister={() => {
+ route("/register");
+ }}
+ />
+ </Fragment>
+ )}
+ />
+ <Route
+ path="/public-accounts"
+ component={() => <PublicHistoriesPage />}
+ />
+ <Route
+ path="/operation/:wopid"
+ component={({ wopid }: { wopid: string }) => (
+ <WithdrawalOperationPage
+ operationId={wopid}
+ onContinue={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ {bankUiSettings.allowRegistrations &&
+ <Route
+ path="/register"
+ component={() => (
+ <RegistrationPage
+ onComplete={() => {
+ route("/account");
+ }}
+ onCancel={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ }
+ <Route default component={Redirect} to="/login" />
+ </Router>
+ </BankFrame>
+ }
+ const { isUserAdministrator, username } = backend.state
+
+ return (
+ <BankFrame account={backend.state.username}>
+ <Router history={history}>
+ <Route
+ path="/operation/:wopid"
+ component={({ wopid }: { wopid: string }) => (
+ <WithdrawalOperationPage
+ operationId={wopid}
+ onContinue={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ <Route
+ path="/public-accounts"
+ component={() => <PublicHistoriesPage />}
+ />
+ <Route
+ path="/account"
+ component={() => {
+ if (isUserAdministrator) {
+ return <AdminHome
+ onRegister={() => {
+ route("/register");
+ }}
+ />;
+ } else {
+ return <HomePage
+ account={username}
+ goToConfirmOperation={(wopid) => {
+ route(`/operation/${wopid}`);
+ }}
+ goToBusinessAccount={() => {
+ route("/business");
+ }}
+ onRegister={() => {
+ route("/register");
+ }}
+ />
+ }
+ }}
+ />
+ <Route
+ path="/business"
+ component={() => (
+ <BusinessAccount
+ account={username}
+ onClose={() => {
+ route("/account");
+ }}
+ onRegister={() => {
+ route("/register");
+ }}
+ onLoadNotOk={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ <Route default component={Redirect} to="/account" />
+ </Router>
+ </BankFrame>
+ );
+}
+
+function Redirect({ to }: { to: string }): VNode {
+ useEffect(() => {
+ route(to, true);
+ }, []);
+ return <div>being redirected to {to}</div>;
+}
+
+export function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
diff --git a/packages/demobank-ui/src/pages/ShowInputErrorLabel.tsx b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
index dacffe20a..c5840cad9 100644
--- a/packages/demobank-ui/src/pages/ShowInputErrorLabel.tsx
+++ b/packages/demobank-ui/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/demobank-ui/src/components/Transactions/index.ts b/packages/demobank-ui/src/components/Transactions/index.ts
index 46b38ce74..9df1a70e5 100644
--- a/packages/demobank-ui/src/components/Transactions/index.ts
+++ b/packages/demobank-ui/src/components/Transactions/index.ts
@@ -46,6 +46,8 @@ export namespace State {
status: "ready";
error: undefined;
transactions: Transaction[];
+ onPrev?: () => void;
+ onNext?: () => void;
}
}
diff --git a/packages/demobank-ui/src/components/Transactions/state.ts b/packages/demobank-ui/src/components/Transactions/state.ts
index 09c039055..4b62b005e 100644
--- a/packages/demobank-ui/src/components/Transactions/state.ts
+++ b/packages/demobank-ui/src/components/Transactions/state.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
+import { AbsoluteTime, Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
import { useTransactions } from "../../hooks/access.js";
import { Props, State, Transaction } from "./index.js";
@@ -34,45 +34,19 @@ export function useComponentState({ account }: Props): State {
}
const transactions = result.data.transactions
- .map((item: unknown) => {
- if (
- !item ||
- typeof item !== "object" ||
- !("direction" in item) ||
- !("creditorIban" in item) ||
- !("debtorIban" in item) ||
- !("date" in item) ||
- !("subject" in item) ||
- !("currency" in item) ||
- !("amount" in item)
- ) {
- //not valid
- return;
- }
- const anyItem = item as any;
- if (
- !(typeof anyItem.creditorIban === "string") ||
- !(typeof anyItem.debtorIban === "string") ||
- !(typeof anyItem.date === "string") ||
- !(typeof anyItem.subject === "string") ||
- !(typeof anyItem.currency === "string") ||
- !(typeof anyItem.amount === "string")
- ) {
- return;
- }
+ .map((tx) => {
- const negative = anyItem.direction === "DBIT";
- const counterpart = negative ? anyItem.creditorIban : anyItem.debtorIban;
+ 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.targetPath.substring(0, 6)}...` : undefined) ??
+ "unkown";
- let date = anyItem.date ? parseInt(anyItem.date, 10) : 0;
- if (isNaN(date) || !isFinite(date)) {
- date = 0;
- }
- const when: AbsoluteTime = !date
- ? AbsoluteTime.never()
- : AbsoluteTime.fromMilliseconds(date);
- const amount = Amounts.parse(`${anyItem.currency}:${anyItem.amount}`);
- const subject = anyItem.subject;
+ const when = AbsoluteTime.fromProtocolTimestamp(tx.date);
+ const amount = Amounts.parse(tx.amount);
+ const subject = tx.subject;
return {
negative,
counterpart,
@@ -87,5 +61,7 @@ export function useComponentState({ account }: Props): State {
status: "ready",
error: undefined,
transactions,
+ onNext: result.isReachingEnd ? undefined : result.loadMore,
+ onPrev: result.isReachingStart ? undefined : result.loadMorePrev,
};
}
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx
index 34d078c16..696fb59f3 100644
--- a/packages/demobank-ui/src/components/Transactions/views.tsx
+++ b/packages/demobank-ui/src/components/Transactions/views.tsx
@@ -14,11 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { State } from "./index.js";
-import { format } from "date-fns";
+import { format, isToday } from "date-fns";
import { Amounts } from "@gnu-taler/taler-util";
+import { useEffect, useRef } from "preact/hooks";
+import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
@@ -30,45 +32,104 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
);
}
-export function ReadyView({ transactions }: State.Ready): VNode {
+export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode {
const { i18n } = useTranslationContext();
+ if (!transactions.length) return <div />
+ const txByDate = transactions.reduce((prev, cur) => {
+ const d = cur.when.t_ms === "never"
+ ? ""
+ : format(cur.when.t_ms, "dd/MM/yyyy")
+ if (!prev[d]) {
+ prev[d] = []
+ }
+ prev[d].push(cur)
+ return prev
+ }, {} as Record<string, typeof transactions>)
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>
- {transactions.map((item, idx) => {
- return (
- <tr key={idx}>
- <td>
- {item.when.t_ms === "never"
- ? ""
- : format(item.when.t_ms, "dd/MM/yyyy HH:mm:ss")}
- </td>
- <td>
- {item.negative ? "-" : ""}
- {item.amount ? (
- `${Amounts.stringifyValue(item.amount)} ${
- item.amount.currency
- }`
- ) : (
- <span style={{ color: "grey" }}>&lt;invalid value&gt;</span>
- )}
- </td>
- <td>{item.counterpart}</td>
- <td>{item.subject}</td>
- </tr>
- );
- })}
- </tbody>
- </table>
+ <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 transactions</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="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>
+ <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 => {
+ const time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss")
+ const amount = <Fragment>
+ { }
+ </Fragment>
+ return (<tr key={idx}>
+ <td class="relative py-2 pl-2 pr-2 text-sm ">
+ <div class="font-medium text-gray-900">{time}</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 ? (
+ <RenderAmount value={item.amount} />
+ ) : (
+ <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>
+ </dl>
+ </td>
+ <td data-negative={item.negative ? "true" : "false"}
+ class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600">
+ {item.amount ? (<RenderAmount value={item.amount} negative={item.negative} />
+ ) : (
+ <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">{item.counterpart}</td>
+ <td class="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
+ 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={!onPrev}
+ onClick={onPrev}
+ >
+ <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>
+ </div>
</div>
);
}
diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx
index ea86da518..7cf658681 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -15,16 +15,23 @@
*/
import {
+ LibtoolVersion,
getGlobalLogLevel,
setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
-import { TranslationProvider } from "@gnu-taler/web-util/browser";
-import { FunctionalComponent, h } from "preact";
+import { TranslationProvider, useApiContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, FunctionalComponent, VNode, h } from "preact";
import { SWRConfig } from "swr";
-import { BackendStateProvider } from "../context/backend.js";
+import { BackendStateProvider, useBackendContext } from "../context/backend.js";
import { strings } from "../i18n/strings.js";
-import { Routing } from "../pages/Routing.js";
-
+import { Routing } from "./Routing.js";
+import { useEffect, useState } from "preact/hooks";
+import { Loading } from "./Loading.js";
+import { getInitialBackendBaseURL } from "../hooks/backend.js";
+import { BANK_INTEGRATION_PROTOCOL_VERSION, useConfigState } from "../hooks/config.js";
+import { ErrorLoading } from "./ErrorLoading.js";
+import { BankFrame } from "../pages/BankFrame.js";
+import { ConfigStateProvider } from "../context/config.js";
const WITH_LOCAL_STORAGE_CACHE = false;
/**
@@ -48,22 +55,44 @@ const App: FunctionalComponent = () => {
return (
<TranslationProvider source={strings}>
<BackendStateProvider>
- <SWRConfig
- value={{
- provider: WITH_LOCAL_STORAGE_CACHE
- ? localStorageProvider
- : undefined,
- }}
- >
- <Routing />
- </SWRConfig>
+ <VersionCheck>
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ }}
+ >
+ <Routing />
+ </SWRConfig>
+ </VersionCheck>
</BackendStateProvider>
- </TranslationProvider>
+ </TranslationProvider >
);
};
(window as any).setGlobalLogLevelFromString = setGlobalLogLevelFromString;
(window as any).getGlobalLevel = getGlobalLogLevel;
+function VersionCheck({ children }: { children: ComponentChildren }): VNode {
+ const checked = useConfigState()
+
+ if (checked === undefined) {
+ return <Loading />
+ }
+ if (checked.type === "wrong") {
+ return <BankFrame>
+ the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}"
+ </BankFrame>
+ }
+ if (checked.type === "ok") {
+ return <ConfigStateProvider value={checked.result}>{children}</ConfigStateProvider>
+ }
+
+ return <BankFrame>
+ <ErrorLoading error={checked.result} />
+ </BankFrame>
+}
+
function localStorageProvider(): Map<unknown, unknown> {
const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
diff --git a/packages/demobank-ui/src/context/backend.ts b/packages/demobank-ui/src/context/backend.ts
index b311ddbb0..eae187c6d 100644
--- a/packages/demobank-ui/src/context/backend.ts
+++ b/packages/demobank-ui/src/context/backend.ts
@@ -34,6 +34,9 @@ const initial: Type = {
logOut() {
null;
},
+ expired() {
+ null;
+ },
logIn(info) {
null;
},
@@ -65,6 +68,7 @@ export const BackendStateProviderTesting = ({
const value: BackendStateHandler = {
state,
logIn: () => {},
+ expired: () => {},
logOut: () => {},
};
diff --git a/packages/demobank-ui/src/scss/_title-bar.scss b/packages/demobank-ui/src/context/config.ts
index 932f8e65d..a2cde18eb 100644
--- a/packages/demobank-ui/src/scss/_title-bar.scss
+++ b/packages/demobank-ui/src/context/config.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,37 +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";
+
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-section.section.is-title-bar {
- padding: $default-padding;
- border-bottom: $light-border;
-
- ul {
- li {
- display: inline-block;
- padding: 0 $default-padding * 0.5 0 0;
- font-size: $default-padding;
- color: $title-bar-color;
-
- &:after {
- display: inline-block;
- content: "/";
- padding-left: $default-padding * 0.5;
- }
-
- &:last-child {
- padding-right: 0;
- font-weight: 900;
- color: $title-bar-active-color;
-
- &:after {
- display: none;
- }
- }
- }
- }
-}
+export type Type = Required<SandboxBackend.Config>;
+
+const initial: Type = {
+ name: "",
+ version: "0:0:0",
+ currency_fraction_digits: 2,
+ currency_fraction_limit: 2,
+ fiat_currency: "",
+ have_cashout: false,
+};
+const Context = createContext<Type>(initial);
+
+export const useConfigContext = (): Type => useContext(Context);
+
+export const ConfigStateProvider = ({
+ value,
+ children,
+}: {
+ value: Type,
+ children: ComponentChildren;
+}): VNode => {
+
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts
index 462287c59..5c55cfade 100644
--- a/packages/demobank-ui/src/declaration.d.ts
+++ b/packages/demobank-ui/src/declaration.d.ts
@@ -74,7 +74,9 @@ type HashCode = string;
type EddsaPublicKey = string;
type EddsaSignature = string;
type WireTransferIdentifierRawP = string;
-type RelativeTime = Duration;
+type RelativeTime = {
+ d_us: number | "forever"
+};
type ImageDataUrl = string;
interface WithId {
@@ -99,20 +101,33 @@ type Amount = string;
type UUID = string;
type Integer = number;
-interface Balance {
- amount: Amount;
- credit_debit_indicator: "credit" | "debit";
-}
-
namespace SandboxBackend {
export interface Config {
// Name of this API, always "circuit".
name: string;
// API version in the form $n:$n:$n
version: string;
- // Contains ratios and fees related to buying
- // and selling the circuit currency.
- ratios_and_fees: RatiosAndFees;
+ // If 'true', the server provides local currency
+ // conversion support.
+ // If missing or false, some parts of the API
+ // are not supported and return 404.
+ have_cashout?: boolean;
+
+ // Fiat currency. That is the currency in which
+ // cash-out operations ultimately wire money.
+ // Only applicable if have_cashout=true.
+ fiat_currency?: string;
+
+ // How many digits should the amounts be rendered
+ // with by default. Small capitals should
+ // be used to render fractions beyond the number
+ // given here (like on gas stations).
+ currency_fraction_digits?: number;
+
+ // How many decimal digits an operation can
+ // have. Wire transfers with more decimal
+ // digits will not be accepted.
+ currency_fraction_limit?: number;
}
interface RatiosAndFees {
// Exchange rate to buy the circuit currency from fiat.
@@ -126,7 +141,7 @@ namespace SandboxBackend {
}
export interface SandboxError {
- error: SandboxErrorDetail;
+ error?: SandboxErrorDetail;
}
interface SandboxErrorDetail {
// String enum classifying the error.
@@ -152,26 +167,12 @@ namespace SandboxBackend {
UtilError = "util-error",
}
- namespace Access {
- interface PublicAccountsResponse {
- publicAccounts: PublicAccount[];
- }
- interface PublicAccount {
- iban: string;
- balance: string;
- // The account name _and_ the username of the
- // Sandbox customer that owns such a bank account.
- accountLabel: string;
- }
- interface BankAccountBalanceResponse {
- // Available balance on the account.
- balance: Balance;
- // payto://-URI of the account. (New)
- paytoUri: string;
- // Number indicating the max debit allowed for the requesting user.
- debitThreshold: Amount;
- }
+ type EmailAddress = string;
+ type PhoneNumber = string;
+
+ namespace CoreBank {
+
interface BankAccountCreateWithdrawalRequest {
// Amount to withdraw.
amount: Amount;
@@ -213,28 +214,24 @@ namespace SandboxBackend {
}
interface BankAccountTransactionInfo {
- creditorIban: string;
- creditorBic: string; // Optional
- creditorName: string;
+ creditor_payto_uri: string;
+ debtor_payto_uri: string;
- debtorIban: string;
- debtorBic: string;
- debtorName: string;
+ amount: Amount;
+ direction: "debit" | "credit";
- amount: number;
- currency: string;
subject: string;
// Transaction unique ID. Matches
// $transaction_id from the URI.
- uid: string;
- direction: "DBIT" | "CRDT";
- date: string; // milliseconds since the Unix epoch
+ row_id: number;
+ date: Timestamp;
}
+
interface CreateBankAccountTransactionCreate {
// Address in the Payto format of the wire transfer receiver.
// It needs at least the 'message' query string parameter.
- paytoUri: string;
+ payto_uri: string;
// Transaction amount (in the $currency:x.y format), optional.
// However, when not given, its value must occupy the 'amount'
@@ -243,11 +240,143 @@ namespace SandboxBackend {
amount?: string;
}
- interface BankRegistrationRequest {
+ 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.
+ internal_payto_uri?: string;
+ }
+ interface ChallengeContactData {
+
+ // E-Mail address
+ email?: EmailAddress;
+
+ // Phone number.
+ phone?: PhoneNumber;
+ }
+
+ 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.
+ 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_address?: string;
+
+ // Legal name associated with $username.
+ // When missing, the old name is kept.
+ name?: string;
+
+ // If present, change the is_exchange configuration.
+ // See RegisterAccountRequest
+ is_exchange?: boolean;
+ }
+
+
+ interface AccountPasswordChange {
+
+ // New password.
+ new_password: string;
}
+ interface PublicAccountsResponse {
+ public_accounts: PublicAccount[];
+ }
+ interface PublicAccount {
+ payto_uri: string;
+
+ balance: Balance;
+
+ // The account name (=username) of the
+ // libeufin-bank account.
+ account_name: string;
+ }
+
+ interface ListBankAccountsResponse {
+ accounts: AccountMinimalData[];
+ }
+ interface Balance {
+ amount: Amount;
+ credit_debit_indicator: "credit" | "debit";
+ }
+ interface AccountMinimalData {
+ // Username
+ username: string;
+
+ // Legal name of the account owner.
+ name: string;
+
+ // current balance of the account
+ balance: Balance;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: Amount;
+ }
+
+ interface AccountData {
+ // Legal name of the account owner.
+ name: string;
+
+ // Available balance on the account.
+ balance: Balance;
+
+ // payto://-URI of the account.
+ payto_uri: string;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: Amount;
+
+ 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;
+ }
+
}
namespace Circuit {
diff --git a/packages/demobank-ui/src/demobank-ui-settings.js b/packages/demobank-ui/src/demobank-ui-settings.js
new file mode 100644
index 000000000..8a0961831
--- /dev/null
+++ b/packages/demobank-ui/src/demobank-ui-settings.js
@@ -0,0 +1,21 @@
+// Values for development environment
+
+/**
+ * Global settings for the demobank UI.
+ */
+localStorage.setItem("bank-base-url", "http://bank.taler.test/");
+
+globalThis.talerDemobankSettings = {
+ backendBaseURL: "http://bank.taler.test/",
+ allowRegistrations: true,
+ showDemoNav: true,
+ simplePasswordForRandomAccounts: true,
+ allowRandomAccountCreation: true,
+ bankName: "Taler DEVELOPMENT Bank",
+ // Names and links for other demo sites to show in the navbar
+ demoSites: [
+ ["Exchange", "https://Exchnage.taler.test/"],
+ ["Bank", "https://bank-ui.taler.test/"],
+ ["Merchant", "https://merchant.taler.test/"],
+ ],
+};
diff --git a/packages/demobank-ui/src/forms/simplest.ts b/packages/demobank-ui/src/forms/simplest.ts
new file mode 100644
index 000000000..54b6b1c65
--- /dev/null
+++ b/packages/demobank-ui/src/forms/simplest.ts
@@ -0,0 +1,66 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString
+} from "@gnu-taler/taler-util";
+import { DoubleColumnForm, FormState } from "@gnu-taler/web-util/browser";
+
+export namespace Data {
+ export interface WithResolution {
+ when: AbsoluteTime;
+ threshold: AmountJson;
+ state: string;
+ }
+ export interface Form extends WithResolution {
+ comment: string;
+ }
+}
+
+const design: DoubleColumnForm = [
+ {
+ title: "Simple form" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "comment",
+ label: "Comments" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title: "Resolution" as TranslatedString,
+ description: `Current state is and threshold at ` as TranslatedString,
+ fields: [
+ {
+ type: "date",
+ props: {
+ name: "when",
+ label: "Decision Time" as TranslatedString,
+ },
+ },
+ {
+ type: "amount",
+ props: {
+ name: "threshold",
+ label: "New threshold" as TranslatedString,
+ },
+ },
+ ],
+ }
+ ,
+];
+
+function formBehavior(v: Partial<Data.Form>): FormState<Data.Form> {
+ return {
+ when: {
+ disabled: true,
+ },
+ threshold: {
+ // disabled: v.state === AmlExchangeBackend.AmlState.frozen,
+ },
+ };
+}
+
+
diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts
index b8b6ab899..154c43ae6 100644
--- a/packages/demobank-ui/src/hooks/access.ts
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -44,13 +44,13 @@ export function useAccessAPI(): AccessAPI {
const account = state.username;
const createWithdrawal = async (
- data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
+ data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest,
): Promise<
- HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
+ HttpResponseOk<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>
> => {
const res =
- await request<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>(
- `access-api/accounts/${account}/withdrawals`,
+ await request<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>(
+ `accounts/${account}/withdrawals`,
{
method: "POST",
data,
@@ -60,21 +60,21 @@ export function useAccessAPI(): AccessAPI {
return res;
};
const createTransaction = async (
- data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
+ data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(
- `access-api/accounts/${account}/transactions`,
+ `accounts/${account}/transactions`,
{
method: "POST",
data,
contentType: "json",
},
);
- await mutateAll(/.*accounts\/.*\/transactions.*/);
+ await mutateAll(/.*accounts\/.*/);
return res;
};
const deleteAccount = async (): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`access-api/accounts/${account}`, {
+ const res = await request<void>(`accounts/${account}`, {
method: "DELETE",
contentType: "json",
});
@@ -94,7 +94,7 @@ export function useAccessAnonAPI(): AccessAnonAPI {
const { request } = useAuthenticatedBackend();
const abortWithdrawal = async (id: string): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`access-api/withdrawals/${id}/abort`, {
+ const res = await request<void>(`withdrawals/${id}/abort`, {
method: "POST",
contentType: "json",
});
@@ -104,7 +104,7 @@ export function useAccessAnonAPI(): AccessAnonAPI {
const confirmWithdrawal = async (
id: string,
): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`access-api/withdrawals/${id}/confirm`, {
+ const res = await request<void>(`withdrawals/${id}/confirm`, {
method: "POST",
contentType: "json",
});
@@ -122,9 +122,10 @@ export function useTestingAPI(): TestingAPI {
const mutateAll = useMatchMutate();
const { request: noAuthRequest } = usePublicBackend();
const register = async (
- data: SandboxBackend.Access.BankRegistrationRequest,
+ data: SandboxBackend.CoreBank.RegisterAccountRequest,
): Promise<HttpResponseOk<void>> => {
- const res = await noAuthRequest<void>(`access-api/testing/register`, {
+ // FIXME: This API is deprecated. The normal account registration API should be used instead.
+ const res = await noAuthRequest<void>(`accounts`, {
method: "POST",
data,
contentType: "json",
@@ -138,18 +139,18 @@ export function useTestingAPI(): TestingAPI {
export interface TestingAPI {
register: (
- data: SandboxBackend.Access.BankRegistrationRequest,
+ data: SandboxBackend.CoreBank.RegisterAccountRequest,
) => Promise<HttpResponseOk<void>>;
}
export interface AccessAPI {
createWithdrawal: (
- data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
+ data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest,
) => Promise<
- HttpResponseOk<SandboxBackend.Access.BankAccountCreateWithdrawalResponse>
+ HttpResponseOk<SandboxBackend.CoreBank.BankAccountCreateWithdrawalResponse>
>;
createTransaction: (
- data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
+ data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate,
) => Promise<HttpResponseOk<void>>;
deleteAccount: () => Promise<HttpResponseOk<void>>;
}
@@ -166,15 +167,15 @@ export interface InstanceTemplateFilter {
export function useAccountDetails(
account: string,
): HttpResponse<
- SandboxBackend.Access.BankAccountBalanceResponse,
+ SandboxBackend.CoreBank.AccountData,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
- HttpResponseOk<SandboxBackend.Access.BankAccountBalanceResponse>,
+ HttpResponseOk<SandboxBackend.CoreBank.AccountData>,
RequestError<SandboxBackend.SandboxError>
- >([`access-api/accounts/${account}`], fetcher, {
+ >([`accounts/${account}`], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -186,28 +187,8 @@ export function useAccountDetails(
keepPreviousData: true,
});
- //FIXME: remove optional when libeufin sandbox has implemented the feature
- if (data && typeof data.data.debitThreshold === "undefined") {
- data.data.debitThreshold = "0";
- }
- //FIXME: sandbox server should return amount string
if (data) {
- const isAmount = Amounts.parse(data.data.debitThreshold);
- if (isAmount) {
- //server response with correct format
- return data;
- }
- const { currency } = Amounts.parseOrThrow(data.data.balance.amount);
- const clone = structuredClone(data);
-
- const theNumber = Number.parseInt(data.data.debitThreshold, 10);
- const value = Number.isNaN(theNumber) ? 0 : theNumber;
- clone.data.debitThreshold = Amounts.stringify({
- currency,
- value: value,
- fraction: 0,
- });
- return clone;
+ return data;
}
if (error) return error.cause;
return { loading: true };
@@ -217,15 +198,15 @@ export function useAccountDetails(
export function useWithdrawalDetails(
wid: string,
): HttpResponse<
- SandboxBackend.Access.BankAccountGetWithdrawalResponse,
+ SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
- HttpResponseOk<SandboxBackend.Access.BankAccountGetWithdrawalResponse>,
+ HttpResponseOk<SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse>,
RequestError<SandboxBackend.SandboxError>
- >([`access-api/withdrawals/${wid}`], fetcher, {
+ >([`withdrawals/${wid}`], fetcher, {
refreshInterval: 1000,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -247,15 +228,15 @@ export function useTransactionDetails(
account: string,
tid: string,
): HttpResponse<
- SandboxBackend.Access.BankAccountTransactionInfo,
+ SandboxBackend.CoreBank.BankAccountTransactionInfo,
SandboxBackend.SandboxError
> {
- const { fetcher } = useAuthenticatedBackend();
+ const { paginatedFetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
- HttpResponseOk<SandboxBackend.Access.BankAccountTransactionInfo>,
+ HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionInfo>,
RequestError<SandboxBackend.SandboxError>
- >([`access-api/accounts/${account}/transactions/${tid}`], fetcher, {
+ >([`accounts/${account}/transactions/${tid}`], paginatedFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -274,13 +255,13 @@ export function useTransactionDetails(
}
interface PaginationFilter {
- page: number;
+ // page: number;
}
export function usePublicAccounts(
args?: PaginationFilter,
): HttpResponsePaginated<
- SandboxBackend.Access.PublicAccountsResponse,
+ SandboxBackend.CoreBank.PublicAccountsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = usePublicBackend();
@@ -292,13 +273,13 @@ export function usePublicAccounts(
error: afterError,
isValidating: loadingAfter,
} = useSWR<
- HttpResponseOk<SandboxBackend.Access.PublicAccountsResponse>,
+ HttpResponseOk<SandboxBackend.CoreBank.PublicAccountsResponse>,
RequestError<SandboxBackend.SandboxError>
- >([`access-api/public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher);
+ >([`public-accounts`, page, PAGE_SIZE], paginatedFetcher);
const [lastAfter, setLastAfter] = useState<
HttpResponse<
- SandboxBackend.Access.PublicAccountsResponse,
+ SandboxBackend.CoreBank.PublicAccountsResponse,
SandboxBackend.SandboxError
>
>({ loading: true });
@@ -311,7 +292,7 @@ export function usePublicAccounts(
// if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd =
- afterData && afterData.data.publicAccounts.length < PAGE_SIZE;
+ afterData && afterData.data.public_accounts.length < PAGE_SIZE;
const isReachingStart = false;
const pagination = {
@@ -319,7 +300,7 @@ export function usePublicAccounts(
isReachingStart,
loadMore: () => {
if (!afterData || isReachingEnd) return;
- if (afterData.data.publicAccounts.length < MAX_RESULT_SIZE) {
+ if (afterData.data.public_accounts.length < MAX_RESULT_SIZE) {
setPage(page + 1);
}
},
@@ -328,12 +309,12 @@ export function usePublicAccounts(
},
};
- const publicAccounts = !afterData
+ const public_accounts = !afterData
? []
- : (afterData || lastAfter).data.publicAccounts;
- if (loadingAfter) return { loading: true, data: { publicAccounts } };
+ : (afterData || lastAfter).data.public_accounts;
+ if (loadingAfter) return { loading: true, data: { public_accounts } };
if (afterData) {
- return { ok: true, data: { publicAccounts }, ...pagination };
+ return { ok: true, data: { public_accounts }, ...pagination };
}
return { loading: true };
}
@@ -348,28 +329,36 @@ export function useTransactions(
account: string,
args?: PaginationFilter,
): HttpResponsePaginated<
- SandboxBackend.Access.BankAccountTransactionsResponse,
+ SandboxBackend.CoreBank.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = useAuthenticatedBackend();
- const [page, setPage] = useState(1);
+ const [start, setStart] = useState<string>();
const {
data: afterData,
error: afterError,
isValidating: loadingAfter,
} = useSWR<
- HttpResponseOk<SandboxBackend.Access.BankAccountTransactionsResponse>,
+ HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionsResponse>,
RequestError<SandboxBackend.SandboxError>
>(
- [`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE],
- paginatedFetcher,
+ [`accounts/${account}/transactions`, start, PAGE_SIZE],
+ paginatedFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+ // revalidateOnMount: false,
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ }
);
const [lastAfter, setLastAfter] = useState<
HttpResponse<
- SandboxBackend.Access.BankAccountTransactionsResponse,
+ SandboxBackend.CoreBank.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
>
>({ loading: true });
@@ -385,19 +374,23 @@ export function useTransactions(
// if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd =
afterData && afterData.data.transactions.length < PAGE_SIZE;
- const isReachingStart = false;
+ const isReachingStart = start == undefined;
const pagination = {
isReachingEnd,
isReachingStart,
loadMore: () => {
if (!afterData || isReachingEnd) return;
- if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
- setPage(page + 1);
- }
+ // if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
+ const l = afterData.data.transactions[afterData.data.transactions.length-1]
+ setStart(String(l.row_id));
+ // }
},
loadMorePrev: () => {
- null;
+ if (!afterData || isReachingStart) return;
+ // if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
+ setStart(undefined)
+ // }
},
};
diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts
index 4b60d1b6c..889618646 100644
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ b/packages/demobank-ui/src/hooks/backend.ts
@@ -40,21 +40,24 @@ import { useCallback, useEffect, useState } from "preact/hooks";
import { useSWRConfig } from "swr";
import { useBackendContext } from "../context/backend.js";
import { bankUiSettings } from "../settings.js";
+import { AccessToken } from "./useCredentialsChecker.js";
/**
* Has the information to reach and
* authenticate at the bank's backend.
*/
-export type BackendState = LoggedIn | LoggedOut;
+export type BackendState = LoggedIn | LoggedOut | Expired;
-export interface BackendCredentials {
+interface LoggedIn {
+ status: "loggedIn";
+ isUserAdministrator: boolean;
username: string;
- password: string;
+ token: AccessToken;
}
-
-interface LoggedIn extends BackendCredentials {
- status: "loggedIn";
+interface Expired {
+ status: "expired";
isUserAdministrator: boolean;
+ username: string;
}
interface LoggedOut {
status: "loggedOut";
@@ -64,10 +67,17 @@ export const codecForBackendStateLoggedIn = (): Codec<LoggedIn> =>
buildCodecForObject<LoggedIn>()
.property("status", codecForConstString("loggedIn"))
.property("username", codecForString())
- .property("password", codecForString())
+ .property("token", codecForString() as Codec<AccessToken>)
.property("isUserAdministrator", codecForBoolean())
.build("BackendState.LoggedIn");
+export const codecForBackendStateExpired = (): Codec<Expired> =>
+ buildCodecForObject<Expired>()
+ .property("status", codecForConstString("expired"))
+ .property("username", codecForString())
+ .property("isUserAdministrator", codecForBoolean())
+ .build("BackendState.Expired");
+
export const codecForBackendStateLoggedOut = (): Codec<LoggedOut> =>
buildCodecForObject<LoggedOut>()
.property("status", codecForConstString("loggedOut"))
@@ -78,6 +88,7 @@ export const codecForBackendState = (): Codec<BackendState> =>
.discriminateOn("status")
.alternative("loggedIn", codecForBackendStateLoggedIn())
.alternative("loggedOut", codecForBackendStateLoggedOut())
+ .alternative("expired", codecForBackendStateExpired())
.build("BackendState");
export function getInitialBackendBaseURL(): string {
@@ -85,18 +96,27 @@ export function getInitialBackendBaseURL(): string {
typeof localStorage !== "undefined"
? localStorage.getItem("bank-base-url")
: undefined;
+ let result: string;
if (!overrideUrl) {
//normal path
if (!bankUiSettings.backendBaseURL) {
console.error(
"ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
);
- return canonicalizeBaseUrl(window.origin);
+ result = window.origin
+ } else {
+ result = bankUiSettings.backendBaseURL;
}
- return canonicalizeBaseUrl(bankUiSettings.backendBaseURL);
+ } else {
+ // testing/development path
+ result = overrideUrl
+ }
+ try {
+ return canonicalizeBaseUrl(result)
+ } catch (e) {
+ //fall back
+ return canonicalizeBaseUrl(window.origin)
}
- // testing/development path
- return canonicalizeBaseUrl(overrideUrl);
}
export const defaultState: BackendState = {
@@ -106,7 +126,8 @@ export const defaultState: BackendState = {
export interface BackendStateHandler {
state: BackendState;
logOut(): void;
- logIn(info: BackendCredentials): void;
+ expired(): void;
+ logIn(info: {username: string, token: AccessToken}): void;
}
const BACKEND_STATE_KEY = buildStorageKey(
@@ -124,12 +145,22 @@ export function useBackendState(): BackendStateHandler {
BACKEND_STATE_KEY,
defaultState,
);
+ const mutateAll = useMatchMutate();
return {
state,
logOut() {
update(defaultState);
},
+ expired() {
+ if (state.status === "loggedOut") return;
+ const nextState: BackendState = {
+ status: "expired",
+ username: state.username,
+ isUserAdministrator: state.username === "admin",
+ };
+ update(nextState);
+ },
logIn(info) {
//admin is defined by the username
const nextState: BackendState = {
@@ -138,6 +169,7 @@ export function useBackendState(): BackendStateHandler {
isUserAdministrator: info.username === "admin",
};
update(nextState);
+ mutateAll(/.*/)
},
};
}
@@ -150,7 +182,7 @@ interface useBackendType {
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
multiFetcher: <T>(endpoint: string[][]) => Promise<HttpResponseOk<T>[]>;
paginatedFetcher: <T>(
- args: [string, number, number],
+ args: [string, string | undefined, number],
) => Promise<HttpResponseOk<T>>;
sandboxAccountsFetcher: <T>(
args: [string, number, number, string],
@@ -179,13 +211,15 @@ export function usePublicBackend(): useBackendType {
[baseUrl],
);
const paginatedFetcher = useCallback(
- function fetcherImpl<T>([endpoint, page, size]: [
+ function fetcherImpl<T>([endpoint, start, size]: [
string,
- number,
+ string | undefined,
number,
]): Promise<HttpResponseOk<T>> {
+ const delta = -1 * size //descending order
+ const params = start ? { delta, start } : { delta }
return requestHandler<T>(baseUrl, endpoint, {
- params: { page: page || 1, size },
+ params,
});
},
[baseUrl],
@@ -247,35 +281,12 @@ interface InvalidationResult {
error: unknown;
}
-export function useCredentialsChecker() {
- const { request } = useApiContext();
- const baseUrl = getInitialBackendBaseURL();
- //check against account details endpoint
- //while sandbox backend doesn't have a login endpoint
- return async function testLogin(
- username: string,
- password: string,
- ): Promise<CheckResult> {
- try {
- await request(baseUrl, `access-api/accounts/${username}/`, {
- basicAuth: { username, password },
- preventCache: true,
- });
- return { valid: true };
- } catch (error) {
- if (error instanceof RequestError) {
- return { valid: false, requestError: true, cause: error.cause };
- }
- return { valid: false, requestError: false, error };
- }
- };
-}
-
export function useAuthenticatedBackend(): useBackendType {
const { state } = useBackendContext();
const { request: requestHandler } = useApiContext();
- const creds = state.status === "loggedIn" ? state : undefined;
+ // FIXME: libeufin returns 400 insteand of 401 if there is no auth token
+ const creds = state.status === "loggedIn" ? state.token : "secret-token:a";
const baseUrl = getInitialBackendBaseURL();
const request = useCallback(
@@ -283,26 +294,28 @@ export function useAuthenticatedBackend(): useBackendType {
path: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, path, { basicAuth: creds, ...options });
+ return requestHandler<T>(baseUrl, path, { token: creds, ...options });
},
[baseUrl, creds],
);
const fetcher = useCallback(
function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, { basicAuth: creds });
+ return requestHandler<T>(baseUrl, endpoint, { token: creds });
},
[baseUrl, creds],
);
const paginatedFetcher = useCallback(
- function fetcherImpl<T>([endpoint, page = 1, size]: [
+ function fetcherImpl<T>([endpoint, start, size]: [
string,
- number,
+ string | undefined,
number,
]): Promise<HttpResponseOk<T>> {
+ const delta = -1 * size //descending order
+ const params = start ? { delta, start } : { delta }
return requestHandler<T>(baseUrl, endpoint, {
- basicAuth: creds,
- params: { page, size },
+ token: creds,
+ params,
});
},
[baseUrl, creds],
@@ -313,7 +326,7 @@ export function useAuthenticatedBackend(): useBackendType {
> {
return Promise.all(
endpoints.map((endpoint) =>
- requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }),
+ requestHandler<T>(baseUrl, endpoint, { token: creds }),
),
);
},
@@ -327,7 +340,7 @@ export function useAuthenticatedBackend(): useBackendType {
string,
]): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, endpoint, {
- basicAuth: creds,
+ token: creds,
params: { page: page || 1, size },
});
},
@@ -339,7 +352,7 @@ export function useAuthenticatedBackend(): useBackendType {
HttpResponseOk<T>
> {
return requestHandler<T>(baseUrl, endpoint, {
- basicAuth: creds,
+ token: creds,
params: { account },
});
},
diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts
index 06557b77f..5dba60951 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -33,6 +33,7 @@ import {
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
import _useSWR, { SWRHook } from "swr";
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
+import { AccessToken } from "./useCredentialsChecker.js";
const useSWR = _useSWR as unknown as SWRHook;
export function useAdminAccountAPI(): AdminAccountAPI {
@@ -90,7 +91,8 @@ export function useAdminAccountAPI(): AdminAccountAPI {
await mutateAll(/.*/);
logIn({
username: account,
- password: data.new_password,
+ //FIXME: change password api
+ token: data.new_password as AccessToken,
});
}
return res;
@@ -215,14 +217,15 @@ export interface CircuitAccountAPI {
async function getBusinessStatus(
request: ReturnType<typeof useApiContext>["request"],
- basicAuth: { username: string; password: string },
+ username: string,
+ token: AccessToken,
): Promise<boolean> {
try {
const url = getInitialBackendBaseURL();
const result = await request<SandboxBackend.Circuit.CircuitAccountData>(
url,
- `circuit-api/accounts/${basicAuth.username}`,
- { basicAuth },
+ `circuit-api/accounts/${username}`,
+ { token },
);
return result.ok;
} catch (error) {
@@ -264,10 +267,10 @@ type CashoutEstimators = {
export function useEstimator(): CashoutEstimators {
const { state } = useBackendContext();
const { request } = useApiContext();
- const basicAuth =
- state.status === "loggedOut"
+ const creds =
+ state.status !== "loggedIn"
? undefined
- : { username: state.username, password: state.password };
+ : state.token;
return {
estimateByCredit: async (amount, fee, rate) => {
const zeroBalance = Amounts.zeroOfCurrency(fee.currency);
@@ -282,7 +285,7 @@ export function useEstimator(): CashoutEstimators {
url,
`circuit-api/cashouts/estimates`,
{
- basicAuth,
+ token: creds,
params: {
amount_credit: Amounts.stringify(amount),
},
@@ -313,7 +316,7 @@ export function useEstimator(): CashoutEstimators {
url,
`circuit-api/cashouts/estimates`,
{
- basicAuth,
+ token: creds,
params: {
amount_debit: Amounts.stringify(amount),
},
@@ -337,13 +340,13 @@ export function useBusinessAccountFlag(): boolean | undefined {
const { state } = useBackendContext();
const { request } = useApiContext();
const creds =
- state.status === "loggedOut"
+ state.status !== "loggedIn"
? undefined
- : { username: state.username, password: state.password };
+ : {user: state.username, token: state.token};
useEffect(() => {
if (!creds) return;
- getBusinessStatus(request, creds)
+ getBusinessStatus(request, creds.user, creds.token)
.then((result) => {
setIsBusiness(result);
})
@@ -432,7 +435,7 @@ export function useBusinessAccounts(
HttpResponseOk<SandboxBackend.Circuit.CircuitAccounts>,
RequestError<SandboxBackend.SandboxError>
>(
- [`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account],
+ [`accounts`, args?.page, PAGE_SIZE, args?.account],
sandboxAccountsFetcher,
{
refreshInterval: 0,
diff --git a/packages/demobank-ui/src/hooks/config.ts b/packages/demobank-ui/src/hooks/config.ts
new file mode 100644
index 000000000..a3bd294db
--- /dev/null
+++ b/packages/demobank-ui/src/hooks/config.ts
@@ -0,0 +1,59 @@
+import { LibtoolVersion } from "@gnu-taler/taler-util";
+import { ErrorType, HttpError, HttpResponseServerError, RequestError, useApiContext } from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { getInitialBackendBaseURL } from "./backend.js";
+
+/**
+ * Protocol version spoken with the bank.
+ *
+ * Uses libtool's current:revision:age versioning.
+ */
+export const BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0";
+
+async function getConfigState(
+ request: ReturnType<typeof useApiContext>["request"],
+): Promise<SandboxBackend.Config> {
+ const url = getInitialBackendBaseURL();
+ const result = await request<SandboxBackend.Config>(url, `config`);
+ return result.data;
+}
+
+export type ConfigResult = undefined
+ | { type: "ok", result: Required<SandboxBackend.Config> }
+ | { type: "wrong", result: SandboxBackend.Config }
+ | { type: "error", result: HttpError<SandboxBackend.SandboxError> }
+
+export function useConfigState(): ConfigResult {
+ const [checked, setChecked] = useState<ConfigResult>()
+ const { request } = useApiContext();
+
+ useEffect(() => {
+ getConfigState(request)
+ .then((result) => {
+ const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version)
+ if (r?.compatible) {
+ const complete: Required<SandboxBackend.Config> = {
+ currency_fraction_digits: result.currency_fraction_digits ?? 2,
+ currency_fraction_limit: result.currency_fraction_limit ?? 2,
+ fiat_currency: "",
+ have_cashout: result.have_cashout ?? false,
+ name: result.name,
+ version: result.version,
+ }
+ setChecked({ type: "ok", result: complete });
+ } else {
+ setChecked({ type: "wrong", result })
+ }
+ })
+ .catch((error: unknown) => {
+ if (error instanceof RequestError) {
+ const result = error.cause
+ setChecked({ type: "error", result });
+ }
+ });
+ }, []);
+
+ return checked;
+}
+
+
diff --git a/packages/demobank-ui/src/hooks/notification.ts b/packages/demobank-ui/src/hooks/notification.ts
deleted file mode 100644
index 9bf621b41..000000000
--- a/packages/demobank-ui/src/hooks/notification.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { memoryMap } from "@gnu-taler/web-util/browser";
-import { StateUpdater, useEffect, useState } from "preact/hooks";
-
-export type NotificationMessage = ErrorNotification | InfoNotification;
-
-//FIXME: this should not be exported since every notification
-// goes throw notify function
-export interface ErrorMessage {
- description?: string;
- title: TranslatedString;
- debug?: string;
-}
-
-interface ErrorNotification {
- type: "error";
- error: ErrorMessage;
-}
-interface InfoNotification {
- type: "info";
- info: TranslatedString;
-}
-
-const storage = memoryMap<NotificationMessage>();
-const NOTIFICATION_KEY = "notification";
-
-export function onNotificationUpdate(
- handler: (newValue: NotificationMessage | undefined) => void,
-) {
- return storage.onUpdate(NOTIFICATION_KEY, () => {
- const newValue = storage.get(NOTIFICATION_KEY);
- handler(newValue);
- });
-}
-
-export function notifyError(error: ErrorMessage) {
- storage.set(NOTIFICATION_KEY, { type: "error", error });
-}
-export function notifyInfo(info: TranslatedString) {
- storage.set(NOTIFICATION_KEY, { type: "info", info });
-}
-
-export function useNotifications(): [
- NotificationMessage | undefined,
- StateUpdater<NotificationMessage | undefined>,
-] {
- const [value, setter] = useState<NotificationMessage | undefined>();
- useEffect(() => {
- return storage.onUpdate(NOTIFICATION_KEY, () => {
- setter(storage.get(NOTIFICATION_KEY));
- });
- });
- return [value, setter];
-}
diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts
index 46b31bf2a..ad853f9d7 100644
--- a/packages/demobank-ui/src/hooks/settings.ts
+++ b/packages/demobank-ui/src/hooks/settings.ts
@@ -15,8 +15,12 @@
*/
import {
+ AmountString,
Codec,
buildCodecForObject,
+ codecForAmountString,
+ codecForBoolean,
+ codecForNumber,
codecForString,
codecOptional,
} from "@gnu-taler/taler-util";
@@ -24,15 +28,33 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
interface Settings {
currentWithdrawalOperationId: string | undefined;
+ showWithdrawalSuccess: boolean;
+ showDemoDescription: boolean;
+ showInstallWallet: boolean;
+ maxWithdrawalAmount: number;
+ fastWithdrawal: boolean;
+ showDebugInfo: boolean;
}
export const codecForSettings = (): Codec<Settings> =>
buildCodecForObject<Settings>()
.property("currentWithdrawalOperationId", codecOptional(codecForString()))
+ .property("showWithdrawalSuccess", (codecForBoolean()))
+ .property("showDemoDescription", (codecForBoolean()))
+ .property("showInstallWallet", (codecForBoolean()))
+ .property("fastWithdrawal", (codecForBoolean()))
+ .property("showDebugInfo", (codecForBoolean()))
+ .property("maxWithdrawalAmount", codecForNumber())
.build("Settings");
const defaultSettings: Settings = {
currentWithdrawalOperationId: undefined,
+ showWithdrawalSuccess: true,
+ showDemoDescription: true,
+ showInstallWallet: true,
+ maxWithdrawalAmount: 25,
+ fastWithdrawal: false,
+ showDebugInfo: false,
};
const DEMOBANK_SETTINGS_KEY = buildStorageKey(
diff --git a/packages/demobank-ui/src/hooks/useCredentialsChecker.ts b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts
new file mode 100644
index 000000000..b3dedb654
--- /dev/null
+++ b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts
@@ -0,0 +1,135 @@
+import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, HttpError, RequestError, useApiContext } from "@gnu-taler/web-util/browser";
+import { getInitialBackendBaseURL } from "./backend.js";
+
+export function useCredentialsChecker() {
+ const { request } = useApiContext();
+ const baseUrl = getInitialBackendBaseURL();
+ //check against instance details endpoint
+ //while merchant backend doesn't have a login endpoint
+ async function requestNewLoginToken(
+ username: string,
+ password: string,
+ ): Promise<LoginResult> {
+ const data: LoginTokenRequest = {
+ scope: "readwrite" as "write", //FIX: different than merchant
+ duration: {
+ // d_us: "forever" //FIX: should return shortest
+ d_us: 60 * 60 * 24 * 7 * 1000 * 1000
+ },
+ refreshable: true,
+ }
+ try {
+ const response = await request<LoginTokenSuccessResponse>(baseUrl, `accounts/${username}/token`, {
+ method: "POST",
+ basicAuth: {
+ username,
+ password,
+ },
+ data,
+ contentType: "json"
+ });
+ return { valid: true, token: `secret-token:${response.data.access_token}` as AccessToken, 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)
+ }
+ return { requestNewLoginToken, refreshLoginToken }
+}
+
+export interface LoginToken {
+ token: AccessToken,
+ expiration: Timestamp,
+}
+// token used to get loginToken
+// must forget after used
+declare const __ac_token: unique symbol;
+export type AccessToken = string & {
+ [__ac_token]: true;
+};
+
+type YesOrNo = "yes" | "no";
+export type LoginResult = {
+ valid: true;
+ token: AccessToken;
+ expiration: Timestamp;
+} | {
+ valid: false;
+ cause: HttpError<{}>;
+}
+
+
+// DELETE /private/instances/$INSTANCE
+export 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;
+}
+export 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.
+ access_token: AccessToken;
+
+ // 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;
+}
diff --git a/packages/demobank-ui/src/index.html b/packages/demobank-ui/src/index.html
index e21e1fccc..315985648 100644
--- a/packages/demobank-ui/src/index.html
+++ b/packages/demobank-ui/src/index.html
@@ -16,27 +16,28 @@
@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>Demobank</title>
- <!-- Optional customization script. -->
- <script src="demobank-ui-settings.js"></script>
- <!-- Entry point for the demobank SPA. -->
- <script type="module" src="index.js"></script>
- <link rel="stylesheet" href="index.css" />
- </head>
- <body>
- <div id="app"></div>
- </body>
-</html>
+<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">
+ <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>Demobank</title>
+ <!-- Optional customization script. -->
+ <script src="demobank-ui-settings.js"></script>
+ <!-- Entry point for the demobank SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+</head>
+
+<body class="h-full">
+ <div id="app"></div>
+</body>
+
+</html> \ No newline at end of file
diff --git a/packages/demobank-ui/src/index.tsx b/packages/demobank-ui/src/index.tsx
index 2e0f740fe..b7d69fd2d 100644
--- a/packages/demobank-ui/src/index.tsx
+++ b/packages/demobank-ui/src/index.tsx
@@ -16,7 +16,7 @@
import App from "./components/app.js";
import { h, render } from "preact";
-import "./scss/main.scss";
+import "./scss/main.css"
const app = document.getElementById("app");
diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx b/packages/demobank-ui/src/pages/AccountPage.tsx
deleted file mode 100644
index 820c59984..000000000
--- a/packages/demobank-ui/src/pages/AccountPage.tsx
+++ /dev/null
@@ -1,170 +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, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
-import {
- ErrorType,
- HttpResponsePaginated,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { Transactions } from "../components/Transactions/index.js";
-import { useBackendContext } from "../context/backend.js";
-import { useAccountDetails } from "../hooks/access.js";
-import { LoginForm } from "./LoginForm.js";
-import { PaymentOptions } from "./PaymentOptions.js";
-import { notifyError } from "../hooks/notification.js";
-import { useEffect, useState } from "preact/hooks";
-
-interface Props {
- account: string;
- onLoadNotOk: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => VNode;
-}
-
-export const CopyIcon = (): VNode => (
- <svg height="16" viewBox="0 0 16 16" width="16">
- <path
- fill-rule="evenodd"
- d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
- />
- <path
- fill-rule="evenodd"
- d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
- />
- </svg>
-);
-
-export const CopiedIcon = (): VNode => (
- <svg height="16" viewBox="0 0 16 16" width="16">
- <path
- fill-rule="evenodd"
- d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
- />
- </svg>
-);
-
-function CopyButton({ getContent }: { getContent: () => string }): VNode {
- const [copied, setCopied] = useState(false);
- function copyText(): void {
- navigator.clipboard.writeText(getContent() || "");
- setCopied(true);
- }
- useEffect(() => {
- if (copied) {
- setTimeout(() => {
- setCopied(false);
- }, 1000);
- }
- }, [copied]);
-
- if (!copied) {
- return (
- <button onClick={copyText} style={{width:32, height:32, fontSize: "initial"}}>
- <CopyIcon />
- </button>
- );
- }
- return (
- <div content="Copied" style={{display:"inline-block"}}>
- <button disabled style={{width:32, height:32 , fontSize: "initial"}}>
- <CopiedIcon />
- </button>
- </div>
- );
-}
-
-
-/**
- * Query account information and show QR code if there is pending withdrawal
- */
-export function AccountPage({ account, onLoadNotOk }: Props): VNode {
- const result = useAccountDetails(account);
- const backend = useBackendContext();
- const { i18n } = useTranslationContext();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- //logout if there is any error, not if loading
- backend.logOut();
- if (result.status === HttpStatusCode.NotFound) {
- notifyError({
- title: i18n.str`Username or account label "${account}" not found`,
- });
- return <LoginForm />;
- }
- return onLoadNotOk(result);
- }
-
- const { data } = result;
- const balance = Amounts.parseOrThrow(data.balance.amount);
- const debitThreshold = Amounts.parseOrThrow(data.debitThreshold);
- const payto = parsePaytoUri(data.paytoUri);
- if (!payto || !payto.isKnown || payto.targetType !== "iban") {
- return (
- <div>Payto from server is not valid &quot;{data.paytoUri}&quot;</div>
- );
- }
- const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
- const limit = balanceIsDebit
- ? Amounts.sub(debitThreshold, balance).amount
- : Amounts.add(balance, debitThreshold).amount;
- return (
- <Fragment>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>
- Welcome, {account} (<a href={stringifyPaytoUri(payto)}>{payto.iban}</a>)! <CopyButton getContent={() => stringifyPaytoUri(payto)} />
- </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">{`${Amounts.stringifyValue(balance)}`}</span>
- &nbsp;
- <span class="currency">{`${balance.currency}`}</span>
- </div>
- )}
- </div>
- </section>
- <section id="payments">
- <div class="payments">
- <h2>{i18n.str`Payments`}</h2>
- <PaymentOptions limit={limit} />
- </div>
- </section>
-
- <section style={{ marginTop: "2em" }}>
- <div class="active">
- <h3>{i18n.str`Latest transactions`}</h3>
- <Transactions account={account} />
- </div>
- </section>
- </Fragment>
- );
-}
diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts
new file mode 100644
index 000000000..9230fb6b1
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AccountPage/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 { HttpError, HttpResponseOk, HttpResponsePaginated, utils } from "@gnu-taler/web-util/browser";
+import { AbsoluteTime, AmountJson, PaytoUriIBAN, PaytoUriTalerBank } from "@gnu-taler/taler-util";
+import { Loading } from "../../components/Loading.js";
+import { useComponentState } from "./state.js";
+import { ReadyView, InvalidIbanView } from "./views.js";
+import { VNode } from "preact";
+import { LoginForm } from "../LoginForm.js";
+import { ErrorLoading } from "../../components/ErrorLoading.js";
+
+export interface Props {
+ account: string;
+ onLoadNotOk: <T>(
+ error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
+ ) => VNode;
+ goToBusinessAccount: () => void;
+ goToConfirmOperation: (id: string) => void;
+}
+
+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: HttpError<SandboxBackend.SandboxError>;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ account: string,
+ limit: AmountJson,
+ goToBusinessAccount: () => void;
+ goToConfirmOperation: (id: string) => void;
+ }
+
+ export interface InvalidIban {
+ status: "invalid-iban",
+ error: HttpResponseOk<SandboxBackend.CoreBank.AccountData>;
+ }
+
+ export interface UserNotFound {
+ status: "error-user-not-found",
+ error: HttpError<any>;
+ onRegister?: () => void;
+ }
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ "error-user-not-found": LoginForm,
+ "invalid-iban": InvalidIbanView,
+ "loading-error": ErrorLoading,
+ ready: ReadyView,
+};
+
+export const AccountPage = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts
new file mode 100644
index 000000000..ca7e1d447
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AccountPage/state.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 { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
+import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useBackendContext } from "../../context/backend.js";
+import { useAccountDetails } from "../../hooks/access.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({ account, goToBusinessAccount, goToConfirmOperation }: Props): State {
+ const result = useAccountDetails(account);
+ const backend = useBackendContext();
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ if (!result.ok) {
+ if (result.loading || result.type === ErrorType.TIMEOUT) {
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+ //logout if there is any error, not if loading
+ // backend.logOut();
+ if (result.status === HttpStatusCode.NotFound) {
+ notifyError(i18n.str`Username or account label "${account}" not found`, undefined);
+ return {
+ status: "error-user-not-found",
+ error: result,
+ };
+ }
+ if (result.status === HttpStatusCode.Unauthorized) {
+ notifyError(i18n.str`Authorization denied`, i18n.str`Maybe the session has expired, login again.`);
+ return {
+ status: "error-user-not-found",
+ error: result,
+ };
+ }
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+
+ const { 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: result
+ };
+ }
+
+ const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
+ const limit = balanceIsDebit
+ ? Amounts.sub(debitThreshold, balance).amount
+ : Amounts.add(balance, debitThreshold).amount;
+
+
+ return {
+ status: "ready",
+ goToBusinessAccount,
+ goToConfirmOperation,
+ error: undefined,
+ account,
+ limit,
+ };
+}
diff --git a/packages/demobank-ui/src/scss/_footer.scss b/packages/demobank-ui/src/pages/AccountPage/stories.tsx
index 112522ed8..f3828a5d6 100644
--- a/packages/demobank-ui/src/scss/_footer.scss
+++ b/packages/demobank-ui/src/pages/AccountPage/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
@@ -19,17 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-footer.footer {
- .logo {
- img {
- width: auto;
- height: $footer-logo-height;
- }
- }
-}
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
-@include mobile {
- .footer-copyright {
- text-align: center;
- }
-}
+export default {
+ title: "account page",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/demobank-ui/src/scss/_mixins.scss b/packages/demobank-ui/src/pages/AccountPage/test.ts
index b52e590e3..588b84c35 100644
--- a/packages/demobank-ui/src/scss/_mixins.scss
+++ b/packages/demobank-ui/src/pages/AccountPage/test.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
@@ -19,16 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-@mixin transition($t) {
- transition: $t 250ms ease-in-out 50ms;
-}
+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";
-@mixin icon-with-update-mark($icon-base-width) {
- .icon {
- width: $icon-base-width;
-
- &.has-update-mark:after {
- right: ($icon-base-width / 2) - 0.85;
- }
- }
-}
+describe("Account states", () => {
+ it("should do some tests", async () => {
+ });
+});
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx
new file mode 100644
index 000000000..483cb579a
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -0,0 +1,93 @@
+/*
+ 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, VNode, h } from "preact";
+import { Attention } from "../../components/Attention.js";
+import { Transactions } from "../../components/Transactions/index.js";
+import { useBusinessAccountDetails } from "../../hooks/circuit.js";
+import { useSettings } from "../../hooks/settings.js";
+import { PaymentOptions } from "../PaymentOptions.js";
+import { State } from "./index.js";
+
+export function InvalidIbanView({ error }: State.InvalidIban) {
+ return (
+ <div>Payto from server is not valid &quot;{error.data.payto_uri}&quot;</div>
+ );
+}
+
+const IS_PUBLIC_ACCOUNT_ENABLED = false
+
+function ShowDemoInfo(): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings();
+ 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 href="/public-accounts">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>
+}
+
+export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> {
+ const { i18n } = useTranslationContext();
+
+ return <Fragment>
+ <MaybeBusinessButton account={account} onClick={goToBusinessAccount} />
+
+ <ShowDemoInfo />
+
+ <PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} />
+ <Transactions account={account} />
+ </Fragment>;
+}
+
+function MaybeBusinessButton({
+ account,
+ onClick,
+}: {
+ account: string;
+ onClick: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useBusinessAccountDetails(account);
+ if (!result.ok) return <Fragment />;
+ return (
+ <div class="w-full flex justify-end">
+ <button
+ 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()
+ onClick()
+ }}
+ >
+ <i18n.Translate>Business Profile</i18n.Translate>
+ </button>
+ </div>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx
deleted file mode 100644
index ce0feebce..000000000
--- a/packages/demobank-ui/src/pages/AdminPage.tsx
+++ /dev/null
@@ -1,1064 +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, parsePaytoUri } from "@gnu-taler/taler-util";
-import {
- ErrorType,
- HttpResponsePaginated,
- RequestError,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { Cashouts } from "../components/Cashouts/index.js";
-import { useBackendContext } from "../context/backend.js";
-import { useAccountDetails } from "../hooks/access.js";
-import {
- useAdminAccountAPI,
- useBusinessAccountDetails,
- useBusinessAccounts,
-} from "../hooks/circuit.js";
-import {
- buildRequestErrorMessage,
- PartialButDefined,
- RecursivePartial,
- undefinedIfEmpty,
- validateIBAN,
- WithIntermediate,
-} from "../utils.js";
-import { ErrorBannerFloat } from "./BankFrame.js";
-import { ShowCashoutDetails } from "./BusinessAccount.js";
-import { handleNotOkResult } from "./HomePage.js";
-import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-import { ErrorMessage, notifyInfo } from "../hooks/notification.js";
-
-const charset =
- "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
-const upperIdx = charset.indexOf("A");
-
-function randomPassword(): string {
- const random = Array.from({ length: 16 }).map(() => {
- return charset.charCodeAt(Math.random() * charset.length);
- });
- // first char can't be upper
- const charIdx = charset.indexOf(String.fromCharCode(random[0]));
- random[0] =
- charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0];
- return String.fromCharCode(...random);
-}
-
-interface Props {
- onRegister: () => void;
-}
-/**
- * Query account information and show QR code if there is pending withdrawal
- */
-export function AdminPage({ onRegister }: Props): VNode {
- const [account, setAccount] = useState<string | undefined>();
- const [showDetails, setShowDetails] = useState<string | undefined>();
- const [showCashouts, setShowCashouts] = useState<string | undefined>();
- const [updatePassword, setUpdatePassword] = useState<string | undefined>();
- const [removeAccount, setRemoveAccount] = useState<string | undefined>();
- const [showCashoutDetails, setShowCashoutDetails] = useState<
- string | undefined
- >();
-
- const [createAccount, setCreateAccount] = useState(false);
-
- const result = useBusinessAccounts({ account });
- const { i18n } = useTranslationContext();
-
- if (result.loading) return <div />;
- if (!result.ok) {
- return handleNotOkResult(i18n, onRegister)(result);
- }
-
- const { customers } = result.data;
-
- if (showCashoutDetails) {
- return (
- <ShowCashoutDetails
- id={showCashoutDetails}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onCancel={() => {
- setShowCashoutDetails(undefined);
- }}
- />
- );
- }
-
- if (showCashouts) {
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Cashout for account {showCashouts}</i18n.Translate>
- </h1>
- </div>
- <Cashouts
- account={showCashouts}
- onSelected={(id) => {
- setShowCashouts(id);
- setShowCashouts(undefined);
- }}
- />
- <p>
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- setShowCashouts(undefined);
- }}
- />
- </p>
- </div>
- );
- }
-
- if (showDetails) {
- return (
- <ShowAccountDetails
- account={showDetails}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onChangePassword={() => {
- setUpdatePassword(showDetails);
- setShowDetails(undefined);
- }}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account updated`);
- setShowDetails(undefined);
- }}
- onClear={() => {
- setShowDetails(undefined);
- }}
- />
- );
- }
- if (removeAccount) {
- return (
- <RemoveAccount
- account={removeAccount}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account removed`);
- setRemoveAccount(undefined);
- }}
- onClear={() => {
- setRemoveAccount(undefined);
- }}
- />
- );
- }
- if (updatePassword) {
- return (
- <UpdateAccountPassword
- account={updatePassword}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Password changed`);
- setUpdatePassword(undefined);
- }}
- onClear={() => {
- setUpdatePassword(undefined);
- }}
- />
- );
- }
- if (createAccount) {
- return (
- <CreateNewAccount
- onClose={() => setCreateAccount(false)}
- onCreateSuccess={(password) => {
- notifyInfo(
- i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
- );
- setCreateAccount(false);
- }}
- />
- );
- }
-
- return (
- <Fragment>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Admin panel</i18n.Translate>
- </h1>
- </div>
-
- <p>
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <div></div>
- <div>
- <input
- class="pure-button pure-button-primary content"
- type="submit"
- value={i18n.str`Create account`}
- onClick={async (e) => {
- e.preventDefault();
-
- setCreateAccount(true);
- }}
- />
- </div>
- </div>
- </p>
-
- <AdminAccount onRegister={onRegister} />
- <section
- id="main"
- style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
- >
- {!customers.length ? (
- <div></div>
- ) : (
- <article>
- <h2>{i18n.str`Accounts:`}</h2>
- <div class="results">
- <table class="pure-table pure-table-striped">
- <thead>
- <tr>
- <th>{i18n.str`Username`}</th>
- <th>{i18n.str`Name`}</th>
- <th>{i18n.str`Balance`}</th>
- <th>{i18n.str`Actions`}</th>
- </tr>
- </thead>
- <tbody>
- {customers.map((item, idx) => {
- const balance = !item.balance
- ? undefined
- : Amounts.parse(item.balance.amount);
- const balanceIsDebit =
- item.balance &&
- item.balance.credit_debit_indicator == "debit";
- return (
- <tr key={idx}>
- <td>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setShowDetails(item.username);
- }}
- >
- {item.username}
- </a>
- </td>
- <td>{item.name}</td>
- <td>
- {!balance ? (
- i18n.str`unknown`
- ) : (
- <span class="amount">
- {balanceIsDebit ? <b>-</b> : null}
- <span class="value">{`${Amounts.stringifyValue(
- balance,
- )}`}</span>
- &nbsp;
- <span class="currency">{`${balance.currency}`}</span>
- </span>
- )}
- </td>
- <td>
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setUpdatePassword(item.username);
- }}
- >
- change password
- </a>
- &nbsp;
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setShowCashouts(item.username);
- }}
- >
- cashouts
- </a>
- &nbsp;
- <a
- href="#"
- onClick={(e) => {
- e.preventDefault();
- setRemoveAccount(item.username);
- }}
- >
- remove
- </a>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </div>
- </article>
- )}
- </section>
- </Fragment>
- );
-}
-
-function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
- const { i18n } = useTranslationContext();
- const r = useBackendContext();
- const account = r.state.status === "loggedIn" ? r.state.username : "admin";
- const result = useAccountDetails(account);
-
- if (!result.ok) {
- return handleNotOkResult(i18n, onRegister)(result);
- }
- const { data } = result;
- const balance = Amounts.parseOrThrow(data.balance.amount);
- const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
- const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
- const limit = balanceIsDebit
- ? Amounts.sub(debitThreshold, balance).amount
- : Amounts.add(balance, debitThreshold).amount;
- if (!balance) return <Fragment />;
- return (
- <Fragment>
- <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">{`${Amounts.stringifyValue(balance)}`}</span>
- &nbsp;
- <span class="currency">{`${balance.currency}`}</span>
- </div>
- )}
- </div>
- </section>
- <PaytoWireTransferForm
- focus
- limit={limit}
- onSuccess={() => {
- notifyInfo(i18n.str`Wire transfer created!`);
- }}
- />
- </Fragment>
- );
-}
-
-const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
-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 ]*$/;
-
-function initializeFromTemplate(
- account: SandboxBackend.Circuit.CircuitAccountData | undefined,
-): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
- const emptyAccount = {
- cashout_address: undefined,
- iban: undefined,
- name: undefined,
- username: undefined,
- contact_data: undefined,
- };
- const emptyContact = {
- email: undefined,
- phone: undefined,
- };
-
- const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
- structuredClone(account) ?? emptyAccount;
- if (typeof initial.contact_data === "undefined") {
- initial.contact_data = emptyContact;
- }
- initial.contact_data.email;
- return initial as any;
-}
-
-export function UpdateAccountPassword({
- account,
- onClear,
- onUpdateSuccess,
- onLoadNotOk,
-}: {
- onLoadNotOk: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => VNode;
- onClear: () => void;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useBusinessAccountDetails(account);
- const { changePassword } = useAdminAccountAPI();
- const [password, setPassword] = useState<string | undefined>();
- const [repeat, setRepeat] = useState<string | undefined>();
- const [error, saveError] = useState<ErrorMessage | undefined>();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return <div>account not found</div>;
- }
- return onLoadNotOk(result);
- }
-
- const errors = undefinedIfEmpty({
- password: !password ? i18n.str`required` : undefined,
- repeat: !repeat
- ? i18n.str`required`
- : password !== repeat
- ? i18n.str`password doesn't match`
- : undefined,
- });
-
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Update password for {account}</i18n.Translate>
- </h1>
- </div>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
-
- <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
- <form class="pure-form">
- <fieldset>
- <label>{i18n.str`Password`}</label>
- <input
- type="password"
- value={password ?? ""}
- onChange={(e) => {
- setPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- </fieldset>
- <fieldset>
- <label>{i18n.str`Repeat password`}</label>
- <input
- type="password"
- value={repeat ?? ""}
- onChange={(e) => {
- setRepeat(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.repeat}
- isDirty={repeat !== undefined}
- />
- </fieldset>
- </form>
- <p>
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <div>
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- onClear();
- }}
- />
- </div>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary content"
- disabled={!!errors}
- type="submit"
- value={i18n.str`Confirm`}
- onClick={async (e) => {
- e.preventDefault();
- if (!!errors || !password) return;
- try {
- const r = await changePassword(account, {
- new_password: password,
- });
- onUpdateSuccess();
- } catch (error) {
- if (error instanceof RequestError) {
- saveError(buildRequestErrorMessage(i18n, error.cause));
- } else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- />
- </div>
- </div>
- </p>
- </div>
- </div>
- );
-}
-
-function CreateNewAccount({
- onClose,
- onCreateSuccess,
-}: {
- onClose: () => void;
- onCreateSuccess: (password: string) => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { createAccount } = useAdminAccountAPI();
- const [submitAccount, setSubmitAccount] = useState<
- SandboxBackend.Circuit.CircuitAccountData | undefined
- >();
- const [error, saveError] = useState<ErrorMessage | undefined>();
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>New account</i18n.Translate>
- </h1>
- </div>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
-
- <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
- <AccountForm
- template={undefined}
- purpose="create"
- onChange={(a) => {
- setSubmitAccount(a);
- }}
- />
-
- <p>
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <div>
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- onClose();
- }}
- />
- </div>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary content"
- disabled={!submitAccount}
- type="submit"
- value={i18n.str`Confirm`}
- onClick={async (e) => {
- e.preventDefault();
-
- if (!submitAccount) return;
- try {
- const account: SandboxBackend.Circuit.CircuitAccountRequest =
- {
- cashout_address: submitAccount.cashout_address,
- contact_data: submitAccount.contact_data,
- internal_iban: submitAccount.iban,
- name: submitAccount.name,
- username: submitAccount.username,
- password: randomPassword(),
- };
-
- await createAccount(account);
- onCreateSuccess(account.password);
- } catch (error) {
- if (error instanceof RequestError) {
- saveError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Forbidden
- ? i18n.str`The rights to perform the operation are not sufficient`
- : status === HttpStatusCode.BadRequest
- ? i18n.str`Input data was invalid`
- : status === HttpStatusCode.Conflict
- ? i18n.str`At least one registration detail was not available`
- : undefined,
- }),
- );
- } else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- />
- </div>
- </div>
- </p>
- </div>
- </div>
- );
-}
-
-export function ShowAccountDetails({
- account,
- onClear,
- onUpdateSuccess,
- onLoadNotOk,
- onChangePassword,
-}: {
- onLoadNotOk: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => VNode;
- onClear?: () => void;
- onChangePassword: () => void;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useBusinessAccountDetails(account);
- const { updateAccount } = useAdminAccountAPI();
- const [update, setUpdate] = useState(false);
- const [submitAccount, setSubmitAccount] = useState<
- SandboxBackend.Circuit.CircuitAccountData | undefined
- >();
- const [error, saveError] = useState<ErrorMessage | undefined>();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return <div>account not found</div>;
- }
- return onLoadNotOk(result);
- }
-
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Business account details</i18n.Translate>
- </h1>
- </div>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
- <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
- <AccountForm
- template={result.data}
- purpose={update ? "update" : "show"}
- onChange={(a) => setSubmitAccount(a)}
- />
-
- <p class="buttons-account">
- <div
- style={{
- display: "flex",
- justifyContent: "space-between",
- flexFlow: "wrap-reverse",
- }}
- >
- <div>
- {onClear ? (
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Close`}
- onClick={async (e) => {
- e.preventDefault();
- onClear();
- }}
- />
- ) : undefined}
- </div>
- <div style={{ display: "flex" }}>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary content"
- disabled={update && !submitAccount}
- type="submit"
- value={i18n.str`Change password`}
- onClick={async (e) => {
- e.preventDefault();
- onChangePassword();
- }}
- />
- </div>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary content"
- disabled={update && !submitAccount}
- type="submit"
- value={update ? i18n.str`Confirm` : i18n.str`Update`}
- onClick={async (e) => {
- e.preventDefault();
-
- if (!update) {
- setUpdate(true);
- } else {
- if (!submitAccount) return;
- try {
- await updateAccount(account, {
- cashout_address: submitAccount.cashout_address,
- contact_data: submitAccount.contact_data,
- });
- onUpdateSuccess();
- } catch (error) {
- if (error instanceof RequestError) {
- saveError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Forbidden
- ? i18n.str`The rights to change the account are not sufficient`
- : status === HttpStatusCode.NotFound
- ? i18n.str`The username was not found`
- : undefined,
- }),
- );
- } else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }
- }}
- />
- </div>
- </div>
- </div>
- </p>
- </div>
- </div>
- );
-}
-
-function RemoveAccount({
- account,
- onClear,
- onUpdateSuccess,
- onLoadNotOk,
-}: {
- onLoadNotOk: <T>(
- error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
- ) => VNode;
- onClear: () => void;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useAccountDetails(account);
- const { deleteAccount } = useAdminAccountAPI();
- const [error, saveError] = useState<ErrorMessage | undefined>();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return <div>account not found</div>;
- }
- return onLoadNotOk(result);
- }
-
- const balance = Amounts.parse(result.data.balance.amount);
- if (!balance) {
- return <div>there was an error reading the balance</div>;
- }
- const isBalanceEmpty = Amounts.isZero(balance);
- return (
- <div>
- <div>
- <h1 class="nav welcome-text">
- <i18n.Translate>Remove account: {account}</i18n.Translate>
- </h1>
- </div>
- {!isBalanceEmpty && (
- <ErrorBannerFloat
- error={{
- title: i18n.str`Can't delete the account`,
- description: i18n.str`Balance is not empty`,
- }}
- onClear={() => saveError(undefined)}
- />
- )}
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
-
- <p>
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <div>
- <input
- class="pure-button"
- type="submit"
- value={i18n.str`Cancel`}
- onClick={async (e) => {
- e.preventDefault();
- onClear();
- }}
- />
- </div>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary content"
- disabled={!isBalanceEmpty}
- type="submit"
- value={i18n.str`Confirm`}
- onClick={async (e) => {
- e.preventDefault();
- try {
- const r = await deleteAccount(account);
- onUpdateSuccess();
- } catch (error) {
- if (error instanceof RequestError) {
- saveError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Forbidden
- ? i18n.str`The administrator specified a institutional username`
- : status === HttpStatusCode.NotFound
- ? i18n.str`The username was not found`
- : status === HttpStatusCode.PreconditionFailed
- ? i18n.str`Balance was not zero`
- : undefined,
- }),
- );
- } else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- />
- </div>
- </div>
- </p>
- </div>
- );
-}
-/**
- * Create valid account object to update or create
- * Take template as initial values for the form
- * Purpose indicate if all field al read only (show), part of them (update)
- * or none (create)
- * @param param0
- * @returns
- */
-function AccountForm({
- template,
- purpose,
- onChange,
-}: {
- template: SandboxBackend.Circuit.CircuitAccountData | undefined;
- onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
- purpose: "create" | "update" | "show";
-}): VNode {
- const initial = initializeFromTemplate(template);
- const [form, setForm] = useState(initial);
- const [errors, setErrors] = useState<
- RecursivePartial<typeof initial> | undefined
- >(undefined);
- const { i18n } = useTranslationContext();
-
- function updateForm(newForm: typeof initial): void {
- const parsed = !newForm.cashout_address
- ? undefined
- : parsePaytoUri(newForm.cashout_address);
-
- const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
- cashout_address: !newForm.cashout_address
- ? i18n.str`required`
- : !parsed
- ? i18n.str`does not follow the pattern`
- : !parsed.isKnown || parsed.targetType !== "iban"
- ? i18n.str`only "IBAN" target are supported`
- : !IBAN_REGEX.test(parsed.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(parsed.iban, i18n),
- contact_data: undefinedIfEmpty({
- email: !newForm.contact_data?.email
- ? i18n.str`required`
- : !EMAIL_REGEX.test(newForm.contact_data.email)
- ? i18n.str`it should be an email`
- : undefined,
- phone: !newForm.contact_data?.phone
- ? i18n.str`required`
- : !newForm.contact_data.phone.startsWith("+")
- ? i18n.str`should start with +`
- : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
- ? i18n.str`phone number can't have other than numbers`
- : undefined,
- }),
- iban: !newForm.iban
- ? undefined //optional field
- : !IBAN_REGEX.test(newForm.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(newForm.iban, i18n),
- name: !newForm.name ? i18n.str`required` : undefined,
- username: !newForm.username ? i18n.str`required` : undefined,
- });
- setErrors(errors);
- setForm(newForm);
- onChange(errors === undefined ? (newForm as any) : undefined);
- }
-
- return (
- <form class="pure-form">
- <fieldset>
- <label for="username">
- {i18n.str`Username`}
- {purpose === "create" && <b style={{ color: "red" }}>*</b>}
- </label>
- <input
- name="username"
- type="text"
- disabled={purpose !== "create"}
- value={form.username}
- onChange={(e) => {
- form.username = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />{" "}
- <ShowInputErrorLabel
- message={errors?.username}
- isDirty={form.username !== undefined}
- />
- </fieldset>
- <fieldset>
- <label>
- {i18n.str`Name`}
- {purpose === "create" && <b style={{ color: "red" }}>*</b>}
- </label>
- <input
- disabled={purpose !== "create"}
- value={form.name ?? ""}
- onChange={(e) => {
- form.name = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />
- <ShowInputErrorLabel
- message={errors?.name}
- isDirty={form.name !== undefined}
- />
- </fieldset>
- {purpose !== "create" && (
- <fieldset>
- <label>{i18n.str`Internal IBAN`}</label>
- <input
- disabled={true}
- value={form.iban ?? ""}
- onChange={(e) => {
- form.iban = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />
- <ShowInputErrorLabel
- message={errors?.iban}
- isDirty={form.iban !== undefined}
- />
- </fieldset>
- )}
- <fieldset>
- <label>
- {i18n.str`Email`}
- {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
- </label>
- <input
- disabled={purpose === "show"}
- value={form.contact_data.email ?? ""}
- onChange={(e) => {
- form.contact_data.email = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />
- <ShowInputErrorLabel
- message={errors?.contact_data?.email}
- isDirty={form.contact_data.email !== undefined}
- />
- </fieldset>
- <fieldset>
- <label>
- {i18n.str`Phone`}
- {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
- </label>
- <input
- disabled={purpose === "show"}
- value={form.contact_data.phone ?? ""}
- onChange={(e) => {
- form.contact_data.phone = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />
- <ShowInputErrorLabel
- message={errors?.contact_data?.phone}
- isDirty={form.contact_data?.phone !== undefined}
- />
- </fieldset>
- <fieldset>
- <label>
- {i18n.str`Cashout address`}
- {purpose !== "show" && <b style={{ color: "red" }}>*</b>}
- </label>
- <input
- disabled={purpose === "show"}
- value={(form.cashout_address ?? "").substring("payto://iban/".length)}
- onChange={(e) => {
- form.cashout_address = "payto://iban/" + e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- />
- <ShowInputErrorLabel
- message={errors?.cashout_address}
- isDirty={form.cashout_address !== undefined}
- />
- </fieldset>
- </form>
- );
-}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index dc61f1302..6ab6ba3e4 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -14,283 +14,362 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Logger, TranslatedString } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { StateUpdater, useEffect, useState } from "preact/hooks";
-import talerLogo from "../assets/logo-white.svg";
-import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js";
+import { Amounts, Logger, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util";
+import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import logo from "../assets/logo-2021.svg";
+import { Attention } from "../components/Attention.js";
+import { CopyButton } from "../components/CopyButton.js";
+import { LangSelector } from "../components/LangSelector.js";
import { useBackendContext } from "../context/backend.js";
-import { useBusinessAccountDetails } from "../hooks/circuit.js";
-import { bankUiSettings } from "../settings.js";
+import { useAccountDetails } from "../hooks/access.js";
import { useSettings } from "../hooks/settings.js";
-import { ErrorMessage, onNotificationUpdate } from "../hooks/notification.js";
+import { bankUiSettings } from "../settings.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
-const IS_PUBLIC_ACCOUNT_ENABLED = false;
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
const versionText = VERSION
? GIT_HASH
- ? `Version ${VERSION} (${GIT_HASH.substring(0, 8)})`
+ ? <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
: "";
-const logger = new Logger("BankFrame");
-
-function MaybeBusinessButton({
- account,
- onClick,
-}: {
- account: string;
- onClick: () => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useBusinessAccountDetails(account);
- if (!result.ok) return <Fragment />;
- return (
- <a
- href="#"
- class="pure-button pure-button-primary"
- onClick={(e) => {
- e.preventDefault();
- onClick();
- }}
- >{i18n.str`Business Profile`}</a>
- );
-}
export function BankFrame({
children,
- goToBusinessAccount,
+ account,
}: {
+ account?: string,
children: ComponentChildren;
- goToBusinessAccount?: () => void;
}): VNode {
const { i18n } = useTranslationContext();
const backend = useBackendContext();
const [settings, updateSettings] = useSettings();
+ const [open, setOpen] = useState(false)
- const demo_sites = [];
- for (const i in bankUiSettings.demoSites)
- demo_sites.push(
- <a href={bankUiSettings.demoSites[i][1]}>
- {bankUiSettings.demoSites[i][0]}
- </a>,
- );
+ const [error, resetError] = useErrorBoundary();
- 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; margin-right: 2em;">
- <h1>
- <span class="it">
- <a href="/">{bankUiSettings.bankName}</a>
- </span>
- </h1>
- {maybeDemoContent(
- <p>
- {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 href="/public-accounts">Public Accounts</a>.
- </i18n.Translate>
- ) : (
- <i18n.Translate>
- This part of the demo shows how a bank that supports Taler
- directly would work.
- </i18n.Translate>
- )}
- </p>,
- )}
- </div>
- </header>
- <div style="display:flex; flex-direction: column;" class="navcontainer">
- <nav class="demolist">
- {maybeDemoContent(<Fragment>{demo_sites}</Fragment>)}
- {backend.state.status === "loggedIn" ? (
- <Fragment>
- {goToBusinessAccount && !backend.state.isUserAdministrator ? (
- <MaybeBusinessButton
- account={backend.state.username}
- onClick={goToBusinessAccount}
- />
- ) : undefined}
-
- <LangSelector />
-
- <a
- href="#"
- class="pure-button logout-button"
- onClick={() => {
- backend.logOut();
- updateSettings("currentWithdrawalOperationId", undefined);
- }}
- >{i18n.str`Logout`}</a>
- </Fragment>
- ) : undefined}
- </nav>
- </div>
- <section id="main" class="content">
- <StatusBanner />
- {children}
- </section>
- <section id="footer" 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. {versionText}{" "}
- <TestingTag />
- </p>
- </section>
- </Fragment>
- );
-}
+ useEffect(() => {
+ if (error) {
+ const desc = (error instanceof Error ? error.stack : String(error)) as TranslatedString
+ if (error instanceof Error) {
+ notifyException(i18n.str`Internal error, please report.`, error)
+ } else {
+ notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString)
+ }
+ resetError()
+ }
+ }, [error])
-function maybeDemoContent(content: VNode): VNode {
- if (bankUiSettings.showDemoNav) {
- return content;
+ const demo_sites = [];
+ if (bankUiSettings.demoSites) {
+ for (const i in bankUiSettings.demoSites)
+ demo_sites.push(
+ <a href={bankUiSettings.demoSites[i][1]}>
+ {bankUiSettings.demoSites[i][0]}
+ </a>,
+ );
}
- return <Fragment />;
-}
-export function ErrorBannerFloat({
- error,
- onClear,
-}: {
- error: ErrorMessage;
- onClear?: () => void;
-}): VNode {
- return (
- <div
- style={{
- position: "fixed",
- top: 10,
- zIndex: 200,
- width: "90%",
- }}
- >
- <ErrorBanner error={error} onClear={onClear} />
- </div>
- );
-}
+ return (<div class="min-h-full flex flex-col m-0" style="min-height: 100vh;">
+ <div class="bg-indigo-600 pb-32">
+ <nav class="">
+ <div class="mx-auto max-w-7xl px-2 sm:px-4 lg:px-8">
+ <div class="relative flex h-16 items-center justify-between ">
+ <div class="flex items-center px-2 lg:px-0">
+ <div class="flex-shrink-0 bg-white rounded-lg">
+ <a href={bankUiSettings.iconLinkURL ?? "#"}>
+ <img
+ class="h-8 w-auto"
+ src={logo}
+ alt="Taler"
+ style={{ height: "1.5rem", margin: ".5rem" }}
+ />
+ </a>
+ </div>
+ {bankUiSettings.demoSites &&
+ <div class="hidden sm:block lg:ml-10 ">
+ <div class="flex space-x-4">
+ {/* <!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" --> */}
+ {bankUiSettings.demoSites.map(([name, url]) => {
+ return <a href={url} class="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>
-function ErrorBanner({
- error,
- onClear,
-}: {
- error: ErrorMessage;
- onClear?: () => void;
-}): VNode {
- return (
- <div
- class="informational informational-fail"
- style={{
- marginTop: 8,
- paddingLeft: 16,
- paddingRight: 16,
- }}
- >
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <p>
- <b>{error.title}</b>
- </p>
- <div style={{ marginTop: "auto", marginBottom: "auto" }}>
- {onClear && (
- <input
- type="button"
- class="pure-button"
- value="Clear"
- onClick={(e) => {
- e.preventDefault();
- onClear();
- }}
- />
- )}
+ <div class="flex">
+ <button type="button" 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">Open settings</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>
</div>
- </div>
- <p>{error.description}</p>
- </div>
- );
-}
-function StatusBanner(): VNode | null {
- const [info, setInfo] = useState<TranslatedString>();
- const [error, setError] = useState<ErrorMessage>();
- useEffect(() => {
- return onNotificationUpdate((newValue) => {
- if (newValue === undefined) {
- setInfo(undefined);
- setError(undefined);
- } else {
- if (newValue.type === "error") {
- setError(newValue.error);
- } else {
- setInfo(newValue.info);
+ {open &&
+ <div class="relative z-10" 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>Preferences</i18n.Translate>
+ </h2>
+ <div class="ml-3 flex h-7 items-center">
+ <button type="button" 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">
+ <li>
+ <a href="#"
+ 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={() => {
+ backend.logOut();
+ setOpen(false)
+ updateSettings("currentWithdrawalOperationId", undefined);
+ }}
+ >
+ <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>
+ <li>
+ <LangSelector />
+ </li>
+ {bankUiSettings.demoSites &&
+ <li class="sm:hidden">
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Sites</i18n.Translate>
+ </div>
+ <ul role="list" class="-mx-2 mt-2 space-y-1">
+ {bankUiSettings.demoSites.map(([name, url]) => {
+ return <li>
+ <a href={url} 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>
+ }
+ <li>
+ <ul role="list" class="space-y-1">
+ <li class="mt-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">
+ <i18n.Translate>Show withdrawal confirmation</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" 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>
+ </li>
+ <li class="mt-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">
+ <i18n.Translate>Show demo description</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={settings.showDemoDescription} 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("showDemoDescription", !settings.showDemoDescription);
+ }}>
+ <span aria-hidden="true" data-enabled={settings.showDemoDescription} 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>
+ <li class="mt-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">
+ <i18n.Translate>Show debug info</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={settings.showDebugInfo} 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("showDebugInfo", !settings.showDebugInfo);
+ }}>
+ <span aria-hidden="true" data-enabled={settings.showDebugInfo} 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>
+ <li class="mt-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">
+ <i18n.Translate>Show install wallet first</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={settings.showInstallWallet} 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("showInstallWallet", !settings.showInstallWallet);
+ }}>
+ <span aria-hidden="true" data-enabled={settings.showInstallWallet} 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>
+ <li class="mt-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">
+ <i18n.Translate>Use fast withdrawal</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={settings.fastWithdrawal} 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("fastWithdrawal", !settings.fastWithdrawal);
+ }}>
+ <span aria-hidden="true" data-enabled={settings.fastWithdrawal} 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>
+ </ul>
+ </nav>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
}
- }
- });
- }, []);
- return (
- <div
- style={{
- position: "fixed",
- top: 10,
- zIndex: 200,
- width: "90%",
- }}
- >
- {!info ? undefined : (
- <div
- class="informational informational-ok"
- style={{ marginTop: 8, paddingLeft: 16, paddingRight: 16 }}
- >
- <div style={{ display: "flex", justifyContent: "space-between" }}>
- <p>
- <b>{info}</b>
- </p>
- <div>
- <input
- type="button"
- class="pure-button"
- value="Clear"
- onClick={async () => {
- setInfo(undefined);
- }}
- />
+ </nav >
+
+ {account &&
+ <header class="py-5 border-t border-indigo-300 border-opacity-25 bg-indigo-600 lg:border-t lg:border-indigo-400 lg:border-opacity-25">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+ <div class=" flex flex-wrap items-center justify-between sm:flex-nowrap">
+ <h3 class="text-2xl font-bold tracking-tight text-white"><WelcomeAccount account={account} /></h3>
+ <div>
+ <h3 class="text-2xl font-bold tracking-tight text-white"><AccountBalance account={account} /></h3>
+ </div>
</div>
</div>
+
+ </header>
+ }
+ </div >
+
+ <StatusBanner />
+ <main class="-mt-32 flex-1">
+ <div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
+ <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
+ {children}
</div>
- )}
- {!error ? undefined : (
- <ErrorBanner
- error={error}
- onClear={() => {
- setError(undefined);
- }}
- />
- )}
- </div>
+ </div>
+ </main>
+
+ <Footer />
+ </div >
+
);
}
+function MaybeShowDebugInfo({ info }: { info: any }): VNode {
+ const [settings] = useSettings()
+ if (settings.showDebugInfo) {
+ return <pre class="whitespace-break-spaces ">
+ {info}
+ </pre>
+ }
+ return <Fragment />
+}
+
+
+function StatusBanner(): VNode {
+ const notifs = useNotifications()
+ if (notifs.length === 0) return <Fragment />
+ return <div class="fixed z-20 w-full p-4"> {
+ notifs.map(n => {
+ switch (n.message.type) {
+ case "error":
+ return <Attention type="danger" title={n.message.title} onClose={() => {
+ n.remove()
+ }}>
+ {n.message.description &&
+ <div class="mt-2 text-sm text-red-700">
+ {n.message.description}
+ </div>
+ }
+ <MaybeShowDebugInfo info={n.message.debug} />
+ {/* <a href="#" class="text-gray-500">
+ show debug info
+ </a>
+ {n.message.debug &&
+ <div class="mt-2 text-sm text-red-700 font-mono break-all">
+ {n.message.debug}
+ </div>
+ } */}
+ </Attention>
+ case "info":
+ return <Attention type="success" title={n.message.title} onClose={() => {
+ n.remove();
+ }} />
+ }
+ })}
+ </div>
+
+}
+
function TestingTag(): VNode {
const testingUrl = localStorage.getItem("bank-base-url");
if (!testingUrl) return <Fragment />;
return (
- <span style={{ color: "gray" }}>
+ <p class="text-xs leading-5 text-gray-300">
Testing with {testingUrl}{" "}
<a
href=""
@@ -302,6 +381,58 @@ function TestingTag(): VNode {
>
stop testing
</a>
- </span>
+ </p>
+ );
+}
+
+function Footer() {
+ const { i18n } = useTranslationContext()
+ return (
+ <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">
+ <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}{" "}
+ <TestingTag />
+ </p>
+ </div>
+ </footer>
);
}
+
+function WelcomeAccount({ account }: { account: string }): VNode {
+ const { i18n } = useTranslationContext();
+
+ const result = useAccountDetails(account);
+ if (!result.ok) return <div />
+
+ const payto = parsePaytoUri(result.data.payto_uri)
+ if (!payto) return <div />
+
+ const accountNumber = !payto.isKnown ? undefined : payto.targetType === "iban" ? payto.iban : payto.targetType === "x-taler-bank" ? payto.account : undefined;
+ return <i18n.Translate>
+ Welcome, {account} {accountNumber !== undefined ?
+ <span>
+ (<a href={result.data.payto_uri}>{accountNumber}</a> <CopyButton getContent={() => result.data.payto_uri} />)
+ </span>
+ : <Fragment />}!
+ </i18n.Translate>
+
+}
+
+function AccountBalance({ account }: { account: string }): VNode {
+ const result = useAccountDetails(account);
+ if (!result.ok) return <div />
+
+ return <RenderAmount
+ value={Amounts.parseOrThrow(result.data.balance.amount)}
+ negative={result.data.balance.credit_debit_indicator === "debit"}
+ />
+}
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx
index 93a9bdfae..95144f086 100644
--- a/packages/demobank-ui/src/pages/HomePage.tsx
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -17,6 +17,7 @@
import {
HttpStatusCode,
Logger,
+ TranslatedString,
parseWithdrawUri,
stringifyWithdrawUri,
} from "@gnu-taler/taler-util";
@@ -24,18 +25,18 @@ import {
ErrorType,
HttpResponse,
HttpResponsePaginated,
+ notify,
+ notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { Loading } from "../components/Loading.js";
-import { useBackendContext } from "../context/backend.js";
import { getInitialBackendBaseURL } from "../hooks/backend.js";
-import { notifyError, notifyInfo } from "../hooks/notification.js";
import { useSettings } from "../hooks/settings.js";
-import { AccountPage } from "./AccountPage.js";
-import { AdminPage } from "./AdminPage.js";
+import { AccountPage } from "./AccountPage/index.js";
import { LoginForm } from "./LoginForm.js";
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
+import { route } from "preact-router";
const logger = new Logger("AccountPage");
@@ -51,73 +52,66 @@ const logger = new Logger("AccountPage");
*/
export function HomePage({
onRegister,
- onPendingOperationFound,
+ account,
+ goToConfirmOperation,
+ goToBusinessAccount,
}: {
- onPendingOperationFound: (id: string) => void;
+ account: string,
onRegister: () => void;
+ goToBusinessAccount: () => void;
+ goToConfirmOperation: (id: string) => void;
}): VNode {
- const backend = useBackendContext();
- const [settings] = useSettings();
const { i18n } = useTranslationContext();
- if (backend.state.status === "loggedOut") {
- return <LoginForm onRegister={onRegister} />;
- }
-
- if (settings.currentWithdrawalOperationId) {
- onPendingOperationFound(settings.currentWithdrawalOperationId);
- return <Loading />;
- }
-
- if (backend.state.isUserAdministrator) {
- return <AdminPage onRegister={onRegister} />;
- }
-
return (
<AccountPage
- account={backend.state.username}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+ account={account}
+ goToConfirmOperation={goToConfirmOperation}
+ goToBusinessAccount={goToBusinessAccount}
+ onLoadNotOk={handleNotOkResult(i18n)}
/>
);
}
export function WithdrawalOperationPage({
operationId,
- onLoadNotOk,
onContinue,
}: {
operationId: string;
- onLoadNotOk: () => void;
onContinue: () => void;
}): VNode {
//FIXME: libeufin sandbox should return show to create the integration api endpoint
//or return withdrawal uri from response
+ const baseUrl = getInitialBackendBaseURL()
const uri = stringifyWithdrawUri({
- bankIntegrationApiBaseUrl: `${getInitialBackendBaseURL()}/integration-api`,
+ bankIntegrationApiBaseUrl: `${baseUrl}/taler-integration`,
withdrawalOperationId: operationId,
});
const parsedUri = parseWithdrawUri(uri);
const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings();
if (!parsedUri) {
- notifyError({
- title: i18n.str`The Withdrawal URI is not valid: "${uri}"`,
- });
+ notifyError(
+ i18n.str`The Withdrawal URI is not valid`,
+ uri as TranslatedString
+ );
return <Loading />;
}
return (
<WithdrawalQRCode
withdrawUri={parsedUri}
- onContinue={onContinue}
- onLoadNotOk={onLoadNotOk}
+ onClose={() => {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onContinue()
+ }}
/>
);
}
export function handleNotOkResult(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
- onRegister?: () => void,
): <T>(
result:
| HttpResponsePaginated<T, SandboxBackend.SandboxError>
@@ -125,53 +119,53 @@ export function handleNotOkResult(
) => VNode {
return function handleNotOkResult2<T>(
result:
- | HttpResponsePaginated<T, SandboxBackend.SandboxError>
- | HttpResponse<T, SandboxBackend.SandboxError>,
+ | HttpResponsePaginated<T, SandboxBackend.SandboxError | undefined>
+ | HttpResponse<T, SandboxBackend.SandboxError | undefined>,
): VNode {
if (result.loading) return <Loading />;
if (!result.ok) {
switch (result.type) {
case ErrorType.TIMEOUT: {
- notifyError({
- title: i18n.str`Request timeout, try again later.`,
- });
+ notifyError(i18n.str`Request timeout, try again later.`, undefined);
break;
}
case ErrorType.CLIENT: {
if (result.status === HttpStatusCode.Unauthorized) {
- notifyError({
- title: i18n.str`Wrong credentials`,
- });
- return <LoginForm onRegister={onRegister} />;
+ notifyError(i18n.str`Wrong credentials`, undefined);
+ return <LoginForm />;
}
const errorData = result.payload;
- notifyError({
- title: i18n.str`Could not load due to a client error`,
- description: errorData.error.description,
+ notify({
+ type: "error",
+ title: i18n.str`Could not load due to a request error`,
+ description: i18n.str`Request to url "${result.info.url}" returned ${result.info.status}`,
debug: JSON.stringify(result),
});
break;
}
case ErrorType.SERVER: {
- notifyError({
+ notify({
+ type: "error",
title: i18n.str`Server returned with error`,
- description: result.payload.error.description,
+ description: result.payload?.error?.description as TranslatedString,
debug: JSON.stringify(result.payload),
});
break;
}
case ErrorType.UNREADABLE: {
- notifyError({
+ notify({
+ type: "error",
title: i18n.str`Unexpected error.`,
- description: `Response from ${result.info?.url} is unreadable, http status: ${result.status}`,
+ description: i18n.str`Response from ${result.info?.url} is unreadable, http status: ${result.status}`,
debug: JSON.stringify(result),
});
break;
}
case ErrorType.UNEXPECTED: {
- notifyError({
+ notify({
+ type: "error",
title: i18n.str`Unexpected error.`,
- description: `Diagnostic from ${result.info?.url} is "${result.message}"`,
+ description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`,
debug: JSON.stringify(result),
});
break;
@@ -180,7 +174,7 @@ export function handleNotOkResult(
assertUnreachable(result);
}
}
-
+ // route("/")
return <div>error</div>;
}
return <div />;
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
index d2cb1bd8e..3ea94b899 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -14,199 +14,249 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpStatusCode } from "@gnu-taler/taler-util";
-import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useBackendContext } from "../context/backend.js";
-import { useCredentialsChecker } from "../hooks/backend.js";
-import { ErrorMessage } from "../hooks/notification.js";
+import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js";
import { bankUiSettings } from "../settings.js";
import { undefinedIfEmpty } from "../utils.js";
-import { ErrorBannerFloat } from "./BankFrame.js";
-import { USERNAME_REGEX } from "./RegistrationPage.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
+
/**
* Collect and submit login data.
*/
export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
const backend = useBackendContext();
- const [username, setUsername] = useState<string | undefined>();
+ const currentUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined
+ const [username, setUsername] = useState<string | undefined>(currentUser);
const [password, setPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext();
- const testLogin = useCredentialsChecker();
- const [error, saveError] = useState<ErrorMessage | undefined>();
+ const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker();
+
+
+ /**
+ * Register form may be shown in the initialization step.
+ * If this is an error when usgin the app the registration
+ * callback is not set
+ */
+ const isSessionExpired = !onRegister
+
+ // useEffect(() => {
+ // if (backend.state.status === "loggedIn") {
+ // backend.expired()
+ // }
+ // },[])
const ref = useRef<HTMLInputElement>(null);
useEffect(function focusInput() {
+ //FIXME: show invalidate session and allow relogin
+ if (isSessionExpired) {
+ localStorage.removeItem("backend-state");
+ window.location.reload()
+ }
ref.current?.focus();
}, []);
+ const [busy, setBusy] = useState<Record<string, undefined>>()
const errors = undefinedIfEmpty({
username: !username
? i18n.str`Missing username`
- : !USERNAME_REGEX.test(username)
- ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ // : !USERNAME_REGEX.test(username)
+ // ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
: undefined,
password: !password ? i18n.str`Missing password` : undefined,
- });
+ }) ?? busy;
+
+ function saveError({ title, description, debug }: { title: TranslatedString, description?: TranslatedString, debug?: any }) {
+ notifyError(title, description, debug)
+ }
+
+ async function doLogout() {
+ backend.logOut()
+ }
+
+ async function doLogin() {
+ if (!username || !password) return;
+ setBusy({})
+ const result = await requestNewLoginToken(username, password);
+ if (result.valid) {
+ backend.logIn({ username, token: result.token });
+ } else {
+ const { cause } = result;
+ switch (cause.type) {
+ case ErrorType.CLIENT: {
+ if (cause.status === HttpStatusCode.Unauthorized) {
+ saveError({
+ title: i18n.str`Wrong credentials for "${username}"`,
+ });
+ } else
+ if (cause.status === HttpStatusCode.NotFound) {
+ saveError({
+ title: i18n.str`Account not found`,
+ });
+ } else {
+ saveError({
+ title: i18n.str`Could not load due to a request error`,
+ description: i18n.str`Request to url "${cause.info.url}" returned ${cause.info.status}`,
+ debug: JSON.stringify(cause.payload),
+ });
+ }
+ break;
+ }
+ case ErrorType.SERVER: {
+ saveError({
+ title: i18n.str`Server had a problem, try again later or report.`,
+ // description: cause.payload.error.description,
+ debug: JSON.stringify(cause.payload),
+ });
+ break;
+ }
+ case ErrorType.TIMEOUT: {
+ saveError({
+ title: i18n.str`Request timeout, try again later.`,
+ });
+ break;
+ }
+ case ErrorType.UNREADABLE: {
+ saveError({
+ title: i18n.str`Unexpected error.`,
+ description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}` as TranslatedString,
+ debug: JSON.stringify(cause),
+ });
+ break;
+ }
+ default: {
+ saveError({
+ title: i18n.str`Unexpected error, please report.`,
+ description: `Diagnostic from ${cause.info?.url} is "${cause.message}"` as TranslatedString,
+ debug: JSON.stringify(cause),
+ });
+ break;
+ }
+ }
+ // backend.logOut();
+ }
+ setPassword(undefined);
+ setBusy(undefined)
+ }
return (
- <Fragment>
- <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
- <div class="login-div">
- <form
- class="login-form"
- noValidate
+ <div class="flex min-h-full flex-col justify-center">
+
+ <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 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 ?? ""}
- enterkeyhint="next"
- placeholder="Username"
- autocomplete="username"
- required
- onInput={(e): void => {
- setUsername(e.currentTarget.value);
+ <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={isSessionExpired}
+ enterkeyhint="next"
+ placeholder="identification"
+ 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">Password</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>
+
+ {isSessionExpired ? <div class="flex justify-between">
+ <button type="submit"
+ 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()
}}
- />
- <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"
- autocomplete="current-password"
- enterkeyhint="send"
- value={password ?? ""}
- placeholder="Password"
- required
- onInput={(e): void => {
- setPassword(e.currentTarget.value);
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <button type="submit"
+ 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={(e) => {
+ e.preventDefault()
+ doLogin()
}}
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- <br />
- <button
- type="submit"
- class="pure-button pure-button-primary"
+ >
+ <i18n.Translate>Renew session</i18n.Translate>
+ </button>
+ </div> : <div>
+ <button type="submit"
+ 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}
- onClick={async (e) => {
- e.preventDefault();
- if (!username || !password) return;
- const testResult = await testLogin(username, password);
- if (testResult.valid) {
- backend.logIn({ username, password });
- } else {
- if (testResult.requestError) {
- const { cause } = testResult;
- switch (cause.type) {
- case ErrorType.CLIENT: {
- if (cause.status === HttpStatusCode.Unauthorized) {
- saveError({
- title: i18n.str`Wrong credentials for "${username}"`,
- });
- }
- if (cause.status === HttpStatusCode.NotFound) {
- saveError({
- title: i18n.str`Account not found`,
- });
- } else {
- saveError({
- title: i18n.str`Could not load due to a client error`,
- description: cause.payload.error.description,
- debug: JSON.stringify(cause.payload),
- });
- }
- break;
- }
- case ErrorType.SERVER: {
- saveError({
- title: i18n.str`Server had a problem, try again later or report.`,
- description: cause.payload.error.description,
- debug: JSON.stringify(cause.payload),
- });
- break;
- }
- case ErrorType.TIMEOUT: {
- saveError({
- title: i18n.str`Request timeout, try again later.`,
- });
- break;
- }
- case ErrorType.UNREADABLE: {
- saveError({
- title: i18n.str`Unexpected error.`,
- description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}`,
- debug: JSON.stringify(cause),
- });
- break;
- }
- default: {
- saveError({
- title: i18n.str`Unexpected error, please report.`,
- description: `Diagnostic from ${cause.info?.url} is "${cause.message}"`,
- debug: JSON.stringify(cause),
- });
- break;
- }
- }
- } else {
- saveError({
- title: i18n.str`Unexpected error, please report.`,
- debug: JSON.stringify(testResult.error),
- });
- }
- backend.logOut();
- }
- setUsername(undefined);
- setPassword(undefined);
+ onClick={(e) => {
+ e.preventDefault()
+ doLogin()
}}
>
- {i18n.str`Login`}
+ <i18n.Translate>Log in</i18n.Translate>
</button>
-
- {bankUiSettings.allowRegistrations && onRegister ? (
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={(e) => {
- e.preventDefault();
- onRegister();
- }}
- >
- {i18n.str`Register`}
- </button>
- ) : (
- <div />
- )}
- </div>
+ </div>}
</form>
+
+ {bankUiSettings.allowRegistrations && onRegister &&
+ <p class="mt-10 text-center text-sm text-gray-500 border-t">
+ <button type="submit"
+ class="flex 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"
+ onClick={(e) => {
+ e.preventDefault()
+ onRegister()
+ }}
+ >
+ <i18n.Translate>Register</i18n.Translate>
+ </button>
+ </p>
+ }
</div>
- </Fragment>
+ </div>
);
}
diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts
new file mode 100644
index 000000000..b347fd942
--- /dev/null
+++ b/packages/demobank-ui/src/pages/OperationState/index.ts
@@ -0,0 +1,122 @@
+/*
+ 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, AmountJson, WithdrawUriResult } from "@gnu-taler/taler-util";
+import { HttpError, utils } from "@gnu-taler/web-util/browser";
+import { ErrorLoading } from "../../components/ErrorLoading.js";
+import { Loading } from "../../components/Loading.js";
+import { useComponentState } from "./state.js";
+import { AbortedView, ConfirmedView, InvalidPaytoView, InvalidReserveView, InvalidWithdrawalView, NeedConfirmationView, ReadyView } from "./views.js";
+
+export interface Props {
+ currency: string;
+ onClose: () => void;
+}
+
+export type State = State.Loading |
+ State.LoadingError |
+ State.Ready |
+ State.Aborted |
+ State.Confirmed |
+ State.InvalidPayto |
+ State.InvalidWithdrawal |
+ State.InvalidReserve |
+ State.NeedConfirmation;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingError {
+ status: "loading-error";
+ error: HttpError<SandboxBackend.SandboxError>;
+ }
+
+ /**
+ * Need to open the wallet
+ */
+ export interface Ready {
+ status: "ready";
+ error: undefined;
+ uri: WithdrawUriResult,
+ onClose: () => void;
+ onAbort: () => void;
+ }
+
+ export interface InvalidPayto {
+ status: "invalid-payto",
+ error: undefined;
+ payto: string | null;
+ onClose: () => void;
+ }
+ export interface InvalidWithdrawal {
+ status: "invalid-withdrawal",
+ error: undefined;
+ onClose: () => void;
+ uri: string,
+ }
+ export interface InvalidReserve {
+ status: "invalid-reserve",
+ error: undefined;
+ onClose: () => void;
+ reserve: string | null;
+ }
+ export interface NeedConfirmation {
+ status: "need-confirmation",
+ onAbort: () => void;
+ onConfirm: () => void;
+ error: undefined;
+ busy: boolean,
+ }
+ export interface Aborted {
+ status: "aborted",
+ error: undefined;
+ onClose: () => void;
+ }
+ export interface Confirmed {
+ status: "confirmed",
+ error: undefined;
+ onClose: () => void;
+ }
+
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ "invalid-payto": InvalidPaytoView,
+ "invalid-withdrawal": InvalidWithdrawalView,
+ "invalid-reserve": InvalidReserveView,
+ "need-confirmation": NeedConfirmationView,
+ "aborted": AbortedView,
+ "confirmed": ConfirmedView,
+ "loading-error": ErrorLoading,
+ ready: ReadyView,
+};
+
+export const OperationState = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts
new file mode 100644
index 000000000..4be680377
--- /dev/null
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -0,0 +1,265 @@
+/*
+ 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, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { RequestError, notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { useAccessAPI, useAccessAnonAPI, useWithdrawalDetails } from "../../hooks/access.js";
+import { getInitialBackendBaseURL } from "../../hooks/backend.js";
+import { useSettings } from "../../hooks/settings.js";
+import { buildRequestErrorMessage } from "../../utils.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({ currency, onClose }: Props): utils.RecursiveState<State> {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+ const { createWithdrawal } = useAccessAPI();
+ const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI();
+ const [busy, setBusy] = useState<Record<string, undefined>>()
+
+ const amount = settings.maxWithdrawalAmount
+
+ async function doSilentStart() {
+ //FIXME: if amount is not enough use balance
+ const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
+
+ try {
+ const result = await createWithdrawal({
+ amount: Amounts.stringify(parsedAmount),
+ });
+ const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
+ if (!uri) {
+ return notifyError(
+ i18n.str`Server responded with an invalid withdraw URI`,
+ i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
+ } else {
+ updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId)
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Forbidden
+ ? i18n.str`The operation was rejected due to insufficient funds`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
+ const withdrawalOperationId = settings.currentWithdrawalOperationId
+ useEffect(() => {
+ if (withdrawalOperationId === undefined) {
+ doSilentStart()
+ }
+ }, [settings.fastWithdrawal, amount])
+
+ const baseUrl = getInitialBackendBaseURL()
+
+ if (!withdrawalOperationId) {
+ return {
+ status: "loading",
+ error: undefined
+ }
+ }
+
+ const wid = withdrawalOperationId
+
+ async function doAbort() {
+ try {
+ setBusy({})
+ await abortWithdrawal(wid);
+ onClose();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+
+ async function doConfirm() {
+ try {
+ setBusy({})
+ await confirmWithdrawal(wid);
+ if (!settings.showWithdrawalSuccess) {
+ notifyInfo(i18n.str`Wire transfer completed!`)
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
+ : status === HttpStatusCode.UnprocessableEntity
+ ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+ const bankIntegrationApiBaseUrl = `${baseUrl}/taler-integration`
+ const uri = stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl,
+ withdrawalOperationId,
+ });
+ const parsedUri = parseWithdrawUri(uri);
+ if (!parsedUri) {
+ return {
+ status: "invalid-withdrawal",
+ error: undefined,
+ uri,
+ onClose,
+ }
+ }
+
+ return (): utils.RecursiveState<State> => {
+ const result = useWithdrawalDetails(withdrawalOperationId);
+ const shouldCreateNewOperation = !result.ok && !result.loading && result.info.status === HttpStatusCode.NotFound
+
+ useEffect(() => {
+ if (shouldCreateNewOperation) {
+ doSilentStart()
+ }
+ }, [])
+ if (!result.ok) {
+ if (result.loading) {
+ return {
+ status: "loading",
+ error: undefined
+ }
+ }
+ if (result.info.status === HttpStatusCode.NotFound) {
+ return {
+ status: "loading",
+ error: undefined,
+ }
+ }
+ return {
+ status: "loading-error",
+ error: result
+ }
+ }
+ const { data } = result;
+ if (data.aborted) {
+ return {
+ status: "aborted",
+ error: undefined,
+ onClose: async () => {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ }
+ }
+
+ if (data.confirmation_done) {
+ if (!settings.showWithdrawalSuccess) {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ }
+ return {
+ status: "confirmed",
+ error: undefined,
+ onClose: async () => {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ }
+ }
+
+ if (!data.selection_done) {
+ return {
+ status: "ready",
+ error: undefined,
+ uri: parsedUri,
+ onClose: async () => {
+ await doAbort()
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ onAbort: doAbort,
+ }
+ }
+
+ if (!data.selected_reserve_pub) {
+ return {
+ status: "invalid-reserve",
+ error: undefined,
+ reserve: data.selected_reserve_pub,
+ onClose,
+ }
+ }
+
+ 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,
+ onClose,
+ }
+ }
+
+
+ // goToConfirmOperation(withdrawalOperationId)
+ return {
+ status: "need-confirmation",
+ error: undefined,
+ onAbort: async () => {
+ await doAbort()
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ busy: !!busy,
+ onConfirm: doConfirm
+ }
+ }
+
+}
diff --git a/packages/demobank-ui/src/scss/_tiles.scss b/packages/demobank-ui/src/pages/OperationState/stories.tsx
index e69d995f0..03917a8fb 100644
--- a/packages/demobank-ui/src/scss/_tiles.scss
+++ b/packages/demobank-ui/src/pages/OperationState/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
@@ -19,6 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-.is-tiles-wrapper {
- margin-bottom: $default-padding;
-}
+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/demobank-ui/src/scss/_modal.scss b/packages/demobank-ui/src/pages/OperationState/test.ts
index b3a31ebf1..f4d6cf4b2 100644
--- a/packages/demobank-ui/src/scss/_modal.scss
+++ b/packages/demobank-ui/src/pages/OperationState/test.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
@@ -19,17 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-.modal-card {
- width: $modal-card-width;
-}
+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";
-.modal-card-foot {
- background-color: $modal-card-foot-background-color;
-}
-
-@include mobile {
- .modal .animation-content .modal-card {
- width: $modal-card-width-mobile;
- margin: 0 auto;
- }
-}
+describe("Withdrawal operation states", () => {
+ it("should do some tests", async () => {
+ });
+});
diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx
new file mode 100644
index 000000000..2cb7385db
--- /dev/null
+++ b/packages/demobank-ui/src/pages/OperationState/views.tsx
@@ -0,0 +1,376 @@
+/*
+ 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 { stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useMemo, useState } from "preact/hooks";
+import { QR } from "../../components/QR.js";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { useSettings } from "../../hooks/settings.js";
+import { undefinedIfEmpty } from "../../utils.js";
+import { State } from "./index.js";
+
+export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) {
+ return (
+ <div>Payto from server is not valid &quot;{payto}&quot;</div>
+ );
+}
+export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) {
+ return (
+ <div>Withdrawal uri from server is not valid &quot;{uri}&quot;</div>
+ );
+}
+export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) {
+ return (
+ <div>Reserve from server is not valid &quot;{reserve}&quot;</div>
+ );
+}
+
+export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State.NeedConfirmation) {
+ const { i18n } = useTranslationContext()
+
+ const captchaNumbers = useMemo(() => {
+ return {
+ a: Math.floor(Math.random() * 10),
+ b: Math.floor(Math.random() * 10),
+ };
+ }, []);
+ const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
+ const answer = parseInt(captchaAnswer ?? "", 10);
+ const errors = undefinedIfEmpty({
+ answer: !captchaAnswer
+ ? i18n.str`Answer the question before continue`
+ : Number.isNaN(answer)
+ ? i18n.str`The answer should be a number`
+ : answer !== captchaNumbers.a + captchaNumbers.b
+ ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
+ : undefined,
+ }) ?? (busy ? {} as Record<string, undefined> : undefined);
+
+ return (
+ <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-2 max-w-xl text-sm text-gray-500">
+ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-4 sm:gap-x-3">
+
+ <label class={"relative sm:col-span-2 flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-noneborder-indigo-600 ring-2 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" />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>challenge response test</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="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>
+ </label>
+
+
+ <label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none 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" />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>using SMS</i18n.Translate>
+ </span>
+ <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>not available</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="h-5 w-5 text-indigo-600 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 class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none 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" />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>one time password</i18n.Translate>
+ </span>
+ <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>not available</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="h-5 w-5 text-indigo-600 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 class="mt-3 text-sm leading-6">
+
+ <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">
+ <label for="withdraw-amount">{i18n.str`What is`}&nbsp;
+ <em>
+ {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
+ </em>
+ ?
+ </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={captchaAnswer ?? ""}
+ required
+
+ name="answer"
+ id="answer"
+ autocomplete="off"
+ onChange={(e): void => {
+ setCaptchaAnswer(e.currentTarget.value)
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== undefined} />
+ </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" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onAbort}
+ >
+ <i18n.Translate>Cancel</i18n.Translate></button>
+ <button type="submit"
+ 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()
+ onConfirm()
+ }}
+ >
+ <i18n.Translate>Transfer</i18n.Translate>
+ </button>
+ </div>
+
+ </form>
+ </div>
+ <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 => {
+ switch (details.account.targetType) {
+ case "iban": {
+ const p = details.account as PaytoUriIBAN
+ const name = p.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">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.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">Exchange name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ </div>
+ }
+ </Fragment>
+ }
+ case "x-taler-bank": {
+ const p = details.account as PaytoUriTalerBank
+ const name = p.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">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.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">Exchange name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ </div>
+ }
+ </Fragment>
+ }
+ default:
+ 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">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</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">Withdrawal identification</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0 break-words">{details.reserve}</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">Amount</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">To be added</dd>
+ // {/* Amounts.stringifyValue(details.amount)
+ </div>
+ </dl>
+ </div>
+ </div> */}
+
+ </div>
+ </div>
+ </div>
+
+ );
+}
+export function AbortedView({ error, onClose }: State.Aborted) {
+ return (
+ <div>aborted</div>
+ );
+}
+
+export function ConfirmedView({ error, onClose }: State.Confirmed) {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+ 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" 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">
+ <button type="button"
+ 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"
+ onClick={async (e) => {
+ e.preventDefault();
+ onClose()
+ }}>
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ </div>
+ </Fragment>
+
+ );
+}
+
+export function ReadyView({ uri, onClose }: State.Ready): 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} ${uri.withdrawalOperationId}`;
+ }, []);
+ const talerWithdrawUri = stringifyWithdrawUri(uri);
+ return <Fragment>
+ <div class="flex justify-end mt-4">
+ <button type="button"
+ 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={() => {
+ onClose()
+ }}
+ >
+ Cancel
+ </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 desktop browser you can open the popup now or click the link if you have the "Inject Taler support" option enabled.</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}
+ 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/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index 3552da7b4..f60ba3270 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -15,10 +15,9 @@
*/
import { AmountJson } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { notifyInfo } from "../hooks/notification.js";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
import { useSettings } from "../hooks/settings.js";
@@ -27,60 +26,97 @@ import { useSettings } from "../hooks/settings.js";
* Let the user choose a payment option,
* then specify the details trigger the action.
*/
-export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
+export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJson, goToConfirmOperation: (id: string) => void }): VNode {
const { i18n } = useTranslationContext();
- const [settings, updateSettings] = useSettings();
+ const [settings] = useSettings();
- const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
- "charge-wallet",
- );
+ const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
return (
- <article>
- <div class="payments">
- <div class="tab">
- <button
- class={tab === "charge-wallet" ? "tablinks active" : "tablinks"}
- onClick={(): void => {
- setTab("charge-wallet");
- }}
- >
- {i18n.str`Withdraw `}
- </button>
- <button
- class={tab === "wire-transfer" ? "tablinks active" : "tablinks"}
- onClick={(): void => {
- setTab("wire-transfer");
- }}
- >
- {i18n.str`Wire transfer`}
- </button>
+ <div class="mt-2">
+
+ <fieldset>
+ <legend class="px-4 text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Send money to</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" --> */}
+ <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")}>
+ <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" onClick={() => {
+ setTab("charge-wallet")
+ }} />
+ <span class="flex flex-1">
+ <div class="text-4xl mr-4 my-auto">&#x1F4B5;</div>
+ <span class="flex flex-col">
+ <span id="project-type-0-label" class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
+ </span>
+ <span id="project-type-0-description-0" 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>
+ </span>
+ {!!settings.currentWithdrawalOperationId &&
+ <span class="inline-flex items-center gap-x-1.5 w-fit rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">
+ <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>
+ }
+ </span>
+ </span>
+ <svg class="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>
+ </label>
+
+
+ <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")}>
+ <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" onClick={() => {
+ setTab("wire-transfer")
+ }} />
+ <span class="flex flex-1">
+ <div class="text-4xl mr-4 my-auto">&#x2194;</div>
+ <span class="flex flex-col">
+ <span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>another bank account</i18n.Translate>
+ </span>
+ <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>Make a wire transfer to an account which you know the bank account number</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="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>
+ </label>
</div>
{tab === "charge-wallet" && (
- <div id="charge-wallet" class="tabcontent active">
- <h3>{i18n.str`Obtain digital cash`}</h3>
- <WalletWithdrawForm
- focus
- limit={limit}
- onSuccess={(id) => {
- updateSettings("currentWithdrawalOperationId", id);
- }}
- />
- </div>
+ <WalletWithdrawForm
+ focus
+ limit={limit}
+ goToConfirmOperation={goToConfirmOperation}
+ onCancel={() => {
+ setTab(undefined)
+ }}
+ />
)}
{tab === "wire-transfer" && (
- <div id="wire-transfer" class="tabcontent active">
- <h3>{i18n.str`Transfer to bank account`}</h3>
- <PaytoWireTransferForm
- focus
- limit={limit}
- onSuccess={() => {
- notifyInfo(i18n.str`Wire transfer created!`);
- }}
- />
- </div>
+ <PaytoWireTransferForm
+ focus
+ title={i18n.str`Transfer details`}
+ limit={limit}
+ onSuccess={() => {
+ notifyInfo(i18n.str`Wire transfer created!`);
+ setTab(undefined)
+ }}
+ onCancel={() => {
+ setTab(undefined)
+ }}
+ />
)}
- </div>
- </article>
- );
+
+ </fieldset>
+ </div>
+ )
}
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index d8c1644b1..52dbd4ff6 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -17,42 +17,51 @@
import {
AmountJson,
Amounts,
- buildPayto,
HttpStatusCode,
Logger,
+ TranslatedString,
+ buildPayto,
parsePaytoUri,
- stringifyPaytoUri,
+ stringifyPaytoUri
} from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { h, VNode, Fragment, Ref } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
-import { notifyError } from "../hooks/notification.js";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAccessAPI } from "../hooks/access.js";
import {
buildRequestErrorMessage,
undefinedIfEmpty,
validateIBAN,
} from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { useConfigState } from "../hooks/config.js";
+import { useConfigContext } from "../context/config.js";
const logger = new Logger("PaytoWireTransferForm");
export function PaytoWireTransferForm({
focus,
+ title,
onSuccess,
+ onCancel,
limit,
}: {
+ title: TranslatedString,
focus?: boolean;
onSuccess: () => void;
+ onCancel: (() => void) | undefined;
limit: AmountJson;
}): VNode {
const [isRawPayto, setIsRawPayto] = useState(false);
- const [iban, setIban] = useState<string | undefined>(undefined);
- const [subject, setSubject] = useState<string | undefined>(undefined);
- const [amount, setAmount] = useState<string | undefined>(undefined);
+ // FIXME: remove this
+ const [iban, setIban] = useState<string | undefined>();
+ const [subject, setSubject] = useState<string | undefined>();
+ const [amount, setAmount] = useState<string | undefined>();
const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
undefined,
@@ -70,295 +79,372 @@ export function PaytoWireTransferForm({
const errorsWire = undefinedIfEmpty({
iban: !iban
- ? i18n.str`Missing IBAN`
+ ? i18n.str`required`
: !IBAN_REGEX.test(iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(iban, i18n),
- subject: !subject ? i18n.str`Missing subject` : undefined,
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : validateIBAN(iban, i18n),
+ subject: !subject ? i18n.str`required` : undefined,
amount: !trimmedAmountStr
- ? i18n.str`Missing amount`
+ ? i18n.str`required`
: !parsedAmount
- ? i18n.str`Amount is not valid`
- : Amounts.isZero(parsedAmount)
- ? i18n.str`Should be greater than 0`
- : Amounts.cmp(limit, parsedAmount) === -1
- ? i18n.str`balance is not enough`
- : undefined,
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedAmount)
+ ? i18n.str`should be greater than 0`
+ : Amounts.cmp(limit, parsedAmount) === -1
+ ? i18n.str`balance is not enough`
+ : undefined,
});
const { createTransaction } = useAccessAPI();
- if (!isRawPayto)
- return (
- <div>
- <form
- class="pure-form"
- name="wire-transfer-form"
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <label for="iban">{i18n.str`Receiver IBAN:`}</label>&nbsp;
- <input
- ref={ref}
- type="text"
- id="iban"
- name="iban"
- value={iban ?? ""}
- placeholder="CC0123456789"
- required
- pattern={ibanRegex}
- onInput={(e): void => {
- setIban(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errorsWire?.iban}
- isDirty={iban !== undefined}
- />
- <label for="subject">{i18n.str`Transfer subject:`}</label>&nbsp;
- <input
- type="text"
- name="subject"
- id="subject"
- placeholder="subject"
- value={subject ?? ""}
- required
- onInput={(e): void => {
- setSubject(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errorsWire?.subject}
- isDirty={subject !== undefined}
- />
- <label for="amount">{i18n.str`Amount:`}</label>&nbsp;
- <div style={{ width: "max-content", display: "flex" }}>
- <input
- type="text"
- readonly
- class="currency-indicator"
- size={limit.currency.length}
- maxLength={limit.currency.length}
- tabIndex={-1}
- style={{
- borderTopRightRadius: 0,
- borderBottomRightRadius: 0,
- borderRight: 0,
- }}
- value={limit.currency}
- />
- <input
- type="number"
- name="amount"
- id="amount"
- placeholder="amount"
- required
- style={{
- borderTopLeftRadius: 0,
- borderBottomLeftRadius: 0,
- borderLeft: 0,
- width: 150,
- }}
- value={amount ?? ""}
- onInput={(e): void => {
- setAmount(e.currentTarget.value);
- }}
- />
- </div>
- <ShowInputErrorLabel
- message={errorsWire?.amount}
- isDirty={amount !== undefined}
- />
- <p style={{ display: "flex", justifyContent: "space-between" }}>
- <input
- type="submit"
- class="pure-button pure-button-primary"
- disabled={!!errorsWire}
- value="Send"
- onClick={async (e) => {
- e.preventDefault();
- if (!(iban && subject && amount)) {
- return;
- }
- const ibanPayto = buildPayto("iban", iban, undefined);
- ibanPayto.params.message = encodeURIComponent(subject);
- const paytoUri = stringifyPaytoUri(ibanPayto);
-
- try {
- await createTransaction({
- paytoUri,
- amount: `${limit.currency}:${amount}`,
- });
- onSuccess();
- setAmount(undefined);
- setIban(undefined);
- setSubject(undefined);
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.BadRequest
- ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- />
- <input
- type="button"
- class="pure-button"
- value="Clear"
- onClick={async (e) => {
- e.preventDefault();
- setAmount(undefined);
- setIban(undefined);
- setSubject(undefined);
- }}
- />
- </p>
- </form>
- <p>
- <a
- href="#"
- onClick={(e) => {
- setIsRawPayto(true);
- e.preventDefault();
- }}
- >
- {i18n.str`Want to try the raw payto://-format?`}
- </a>
- </p>
- </div>
- );
-
const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
const errorsPayto = undefinedIfEmpty({
rawPaytoInput: !rawPaytoInput
? i18n.str`required`
: !parsed
- ? i18n.str`does not follow the pattern`
- : !parsed.params.amount
- ? i18n.str`use the "amount" parameter to specify the amount to be transferred`
- : Amounts.parse(parsed.params.amount) === undefined
- ? i18n.str`the amount is not valid`
- : !parsed.params.message
- ? i18n.str`use the "message" parameter to specify a reference text for the transfer`
- : !parsed.isKnown || parsed.targetType !== "iban"
- ? i18n.str`only "IBAN" target are supported`
- : !IBAN_REGEX.test(parsed.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(parsed.iban, i18n),
+ ? i18n.str`does not follow the pattern`
+ : !parsed.isKnown || parsed.targetType !== "iban"
+ ? i18n.str`only "IBAN" target are supported`
+ : !parsed.params.amount
+ ? i18n.str`use the "amount" parameter to specify the amount to be transferred`
+ : Amounts.parse(parsed.params.amount) === undefined
+ ? i18n.str`the amount is not valid`
+ : !parsed.params.message
+ ? i18n.str`use the "message" parameter to specify a reference text for the transfer`
+ : !IBAN_REGEX.test(parsed.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : validateIBAN(parsed.iban, i18n),
});
- return (
- <div>
- <p>{i18n.str`Transfer money to account identified by payto:// URI:`}</p>
- <form
- class="pure-form"
- name="payto-form"
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <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 style={{ fontSize: "small", marginTop: 4 }}>
- Hint:
- <code>
- payto://iban/[receiver-iban]?message=[subject]&amount=[
- {limit.currency}
- :X.Y]
- </code>
- </div>
- </p>
- <p>
- <input
- class="pure-button pure-button-primary"
- type="button"
- disabled={!!errorsPayto}
- value={i18n.str`Send`}
- onClick={async () => {
- if (!rawPaytoInput) {
- logger.error("Didn't get any raw Payto string!");
- return;
+ async function doSend() {
+ let payto_uri: string | undefined;
+
+ if (rawPaytoInput) {
+ payto_uri = rawPaytoInput
+ } else {
+ if (!iban || !subject) return;
+ const ibanPayto = buildPayto("iban", iban, undefined);
+ ibanPayto.params.message = encodeURIComponent(subject);
+ payto_uri = stringifyPaytoUri(ibanPayto);
+ }
+
+ try {
+ await createTransaction({
+ payto_uri,
+ amount: `${limit.currency}:${amount}`,
+ });
+ onSuccess();
+ setAmount(undefined);
+ setIban(undefined);
+ setSubject(undefined);
+ rawPaytoInputSetter(undefined)
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.BadRequest
+ ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+
+ }
+
+ return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+ {/**
+ * FIXME: Scan a qr code
+ */}
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ {title}
+ </h2>
+ <div>
+ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-1 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={() => {
+ if (parsed && parsed.isKnown && parsed.targetType === "iban") {
+ setIban(parsed.iban)
+ const amount = Amounts.parse(parsed.params["amount"])
+ if (amount) {
+ setAmount(Amounts.stringifyValue(amount))
+ }
+ const subject = parsed.params["subject"]
+ if (subject) {
+ setSubject(subject)
+ }
}
+ 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>
- try {
- await createTransaction({
- paytoUri: rawPaytoInput,
- });
- onSuccess();
- rawPaytoInputSetter(undefined);
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.BadRequest
- ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ <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={() => {
+ if (iban) {
+ const payto = buildPayto("iban", iban, undefined)
+ if (parsedAmount) {
+ payto.params["amount"] = Amounts.stringify(parsedAmount)
+ }
+ if (subject) {
+ payto.params["message"] = subject
}
+ rawPaytoInputSetter(stringifyPaytoUri(payto))
}
- }}
- />
- </p>
- <p>
- <a
- href="/account"
- onClick={() => {
- setIsRawPayto(false);
- }}
+ 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>
+ </div>
+ </div>
+ </div>
+
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 w-fit mx-auto"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={e => {
+ e.preventDefault()
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ {!isRawPayto ?
+ <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="iban" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Recipient`}</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 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ value={iban ?? ""}
+ placeholder="CC0123456789"
+ autocomplete="off"
+ required
+ pattern={ibanRegex}
+ onInput={(e): void => {
+ setIban(e.currentTarget.value.toUpperCase());
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsWire?.iban}
+ isDirty={iban !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>IBAN of the recipient's account</i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label for="subject" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Transfer subject`}</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="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" >some text to identify the transfer</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`}</label>
+ <InputAmount
+ name="amount"
+ left
+ currency={limit.currency}
+ value={trimmedAmountStr}
+ onChange={(d) => {
+ setAmount(d)
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsWire?.amount}
+ isDirty={subject !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500" >amount to transfer</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:`}</label>
+ <div class="mt-2">
+ <textarea
+ ref={focus ? doAutoFocus : undefined}
+ name="address"
+ id="address"
+ type="textarea"
+ rows={3}
+ class="block overflow-hidden w-64 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
+ placeholder={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">
+ {onCancel ?
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onCancel}
>
- {i18n.str`Use wire-transfer form?`}
- </a>
- </p>
- </form>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ : <div />
+ }
+ <button type="submit"
+ 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>
+ </form>
+ </div >
+ )
+
+}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null) {
+ if (element) {
+ setTimeout(() => {
+ element.focus()
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center"
+ })
+ }, 100)
+ }
+}
+
+const FRAC_SEPARATOR = "."
+
+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 cfg = useConfigContext()
+ 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="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 > cfg.currency_fraction_limit) {
+ e.currentTarget.value = e.currentTarget.value.substring(0, sep_pos + cfg.currency_fraction_limit + 1)
+ }
+ onChange(e.currentTarget.value);
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
</div>
);
}
+
+export function RenderAmount({ value, negative }: { value: AmountJson, negative?: boolean }): VNode {
+ const cfg = useConfigContext()
+ const str = Amounts.stringifyValue(value)
+ const sep_pos = str.indexOf(FRAC_SEPARATOR)
+ if (sep_pos !== -1 && str.length - sep_pos - 1 > cfg.currency_fraction_digits) {
+ const limit = sep_pos + cfg.currency_fraction_digits + 1
+ const normal = str.substring(0, limit)
+ const small = str.substring(limit)
+ return <span class="whitespace-nowrap">
+ {negative ? "-" : undefined}
+ {value.currency} {normal} <sup class="-ml-2">{small}</sup>
+ </span>
+ }
+ return <span class="whitespace-nowrap">
+ {negative ? "-" : undefined}
+ {value.currency} {str}
+ </span>
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
index 03bdb78b7..680368919 100644
--- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
@@ -36,8 +36,8 @@ export function PublicHistoriesPage({}: Props): VNode {
const result = usePublicAccounts();
const [showAccount, setShowAccount] = useState(
- result.ok && result.data.publicAccounts.length > 0
- ? result.data.publicAccounts[0].accountLabel
+ result.ok && result.data.public_accounts.length > 0
+ ? result.data.public_accounts[0].account_name
: undefined,
);
@@ -51,9 +51,9 @@ export function PublicHistoriesPage({}: Props): VNode {
const accountsBar = [];
// 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;
+ for (const account of data.public_accounts) {
+ logger.trace("Asking transactions for", account.account_name);
+ const isSelected = account.account_name == showAccount;
accountsBar.push(
<li
class={
@@ -65,13 +65,13 @@ export function PublicHistoriesPage({}: Props): VNode {
<a
href="#"
class="pure-menu-link"
- onClick={() => setShowAccount(account.accountLabel)}
+ onClick={() => setShowAccount(account.account_name)}
>
- {account.accountLabel}
+ {account.account_name}
</a>
</li>,
);
- txs[account.accountLabel] = <Transactions account={account.accountLabel} />;
+ txs[account.account_name] = <Transactions account={account.account_name} />;
}
return (
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index c27984569..e07525ab4 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -17,17 +17,19 @@
import {
HttpStatusCode,
stringifyWithdrawUri,
+ TranslatedString,
WithdrawUriResult,
} from "@gnu-taler/taler-util";
import {
+ notify,
+ notifyError,
RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useEffect } from "preact/hooks";
import { QR } from "../components/QR.js";
import { useAccessAnonAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { buildRequestErrorMessage } from "../utils.js";
export function QrCodeSection({
@@ -49,47 +51,87 @@ export function QrCodeSection({
const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
const { abortWithdrawal } = useAccessAnonAPI();
+
+ async function doAbort() {
+ try {
+ await abortWithdrawal(withdrawUri.withdrawalOperationId);
+ onAborted();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
return (
- <section id="main" class="content">
- <h1 class="nav">{i18n.str`Charge your GNU Taler wallet`}</h1>
- <article>
- <div class="qr-div ">
- <a href={talerWithdrawUri} class="pure-button pure-button-primary">
- <i18n.Translate>Continue with GNU Taler</i18n.Translate>
- </a>
- <p>{i18n.str`Or scan this QR code with your mobile to receive the coin in another device:`}</p>
- <QR text={talerWithdrawUri} />
- <a
- class="pure-button btn-cancel"
- onClick={async (e) => {
- e.preventDefault();
- try {
- await abortWithdrawal(withdrawUri.withdrawalOperationId);
- onAborted();
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Conflict
- ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- >{i18n.str`Cancel`}</a>
+ <Fragment>
+ <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 from <a class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net/en/wallet.html">here</a>.
+ </i18n.Translate></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"
+ // 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"
+ onClick={doAbort}
+ >
+ Cancel
+ </button>
+ <a href={talerWithdrawUri}
+ 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 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>
- </article>
- </section>
+ <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"
+ onClick={doAbort}
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+
+ </Fragment>
);
}
+
+
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index ded48564f..9ac93bb34 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -13,26 +13,31 @@
You should have received a 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, Logger } from "@gnu-taler/taler-util";
+import { HttpStatusCode, Logger, TranslatedString } from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
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 { useTestingAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { bankUiSettings } from "../settings.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
+import { getRandomPassword, getRandomUsername } from "./rnd.js";
+import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js";
const logger = new Logger("RegistrationPage");
export function RegistrationPage({
onComplete,
+ onCancel
}: {
onComplete: () => void;
+ onCancel: () => void;
}): VNode {
const { i18n } = useTranslationContext();
if (!bankUiSettings.allowRegistrations) {
@@ -40,168 +45,357 @@ export function RegistrationPage({
<p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
);
}
- return <RegistrationForm onComplete={onComplete} />;
+ return <RegistrationForm onComplete={onComplete} onCancel={onCancel} />;
}
-export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/;
+export const USERNAME_REGEX = /^[a-z][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({ onComplete }: { onComplete: () => void }): VNode {
+function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, onCancel: () => void }): VNode {
const backend = useBackendContext();
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 { requestNewLoginToken } = useCredentialsChecker()
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 and numbers only, and start with a lowercase letter`
- : undefined,
+ ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ : 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,
+ ? i18n.str`Passwords don't match`
+ : undefined,
});
+ async function doRegistrationStep() {
+ if (!username || !password) return;
+ try {
+ await register({ name: name ?? "", username, password });
+ const resp = await requestNewLoginToken(username, password)
+ setUsername(undefined);
+ if (resp.valid) {
+ backend.logIn({ username, token: resp.token });
+ }
+ onComplete();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`That username is already taken`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setPassword(undefined);
+ setRepeatPassword(undefined);
+ }
+
+ async function delay(ms: number): Promise<void> {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(undefined);
+ }, ms)
+ })
+ }
+ async function doRandomRegistration(tries: number = 3) {
+ const user = getRandomUsername();
+ const pass = getRandomPassword();
+ try {
+ setUsername(undefined);
+ setPassword(undefined);
+ setRepeatPassword(undefined);
+ const username = `_${user.first}-${user.second}_`
+ await register({ username, name: `${user.first} ${user.second}`, password: pass });
+ const resp = await requestNewLoginToken(username, pass)
+ if (resp.valid) {
+ backend.logIn({ username, token: resp.token });
+ }
+ onComplete();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ if (tries > 0) {
+ await delay(200)
+ await doRandomRegistration(tries - 1)
+ } else {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`Could not create a random user`
+ : undefined,
+ }),
+ );
+ }
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
return (
<Fragment>
- <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
- <article>
- <div class="register-div">
- <form
- class="register-form"
- noValidate
+ <h1 class="nav"></h1>
+
+ <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 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"
- autocomplete="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"
- autocomplete="new-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"
- autocomplete="new-password"
- 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"
- type="submit"
- disabled={!!errors}
- onClick={async (e) => {
- e.preventDefault();
-
- if (!username || !password) return;
- try {
- const credentials = { username, password };
- await register(credentials);
- setUsername(undefined);
- setPassword(undefined);
- setRepeatPassword(undefined);
- backend.logIn(credentials);
- onComplete();
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Conflict
- ? i18n.str`That username is already taken`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
+ <div>
+ <label for="username" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>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="identification"
+ 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>
+ <label for="name" class="block text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>Name</i18n.Translate>
+ </label>
+ <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="your name"
+ 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">
+ <button type="submit"
+ 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"
+ onClick={(e) => {
+ e.preventDefault()
+ onCancel()
}}
>
- {i18n.str`Register`}
+ <i18n.Translate>Cancel</i18n.Translate>
</button>
- {/* FIXME: should use a different color */}
- <button
- class="pure-button pure-button-secondary btn-cancel"
+ <button type="submit"
+ 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={(e) => {
- e.preventDefault();
- setUsername(undefined);
- setPassword(undefined);
- setRepeatPassword(undefined);
- onComplete();
+ e.preventDefault()
+ doRegistrationStep()
}}
>
- {i18n.str`Cancel`}
+ <i18n.Translate>Register</i18n.Translate>
</button>
</div>
+
</form>
+
+ {bankUiSettings.allowRandomAccountCreation &&
+ <p class="mt-10 text-center text-sm text-gray-500 border-t">
+ <button type="submit"
+ 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 user</i18n.Translate>
+ </button>
+ </p>
+ }
</div>
- </article>
+ </div>
+
</Fragment>
);
}
diff --git a/packages/demobank-ui/src/pages/Routing.tsx b/packages/demobank-ui/src/pages/Routing.tsx
deleted file mode 100644
index f176c73db..000000000
--- a/packages/demobank-ui/src/pages/Routing.tsx
+++ /dev/null
@@ -1,110 +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 { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { createHashHistory } from "history";
-import { VNode, h } from "preact";
-import { Route, Router, route } from "preact-router";
-import { useEffect, useMemo, useState } from "preact/hooks";
-import { BankFrame } from "./BankFrame.js";
-import { BusinessAccount } from "./BusinessAccount.js";
-import { HomePage, WithdrawalOperationPage } from "./HomePage.js";
-import { PublicHistoriesPage } from "./PublicHistoriesPage.js";
-import { RegistrationPage } from "./RegistrationPage.js";
-
-export function Routing(): VNode {
- const history = createHashHistory();
-
- return (
- <BankFrame
- goToBusinessAccount={() => {
- route("/business");
- }}
- >
- <Router history={history}>
- <Route
- path="/operation/:wopid"
- component={({ wopid }: { wopid: string }) => (
- <WithdrawalOperationPage
- operationId={wopid}
- onContinue={() => {
- route("/account");
- }}
- onLoadNotOk={() => {
- route("/account");
- }}
- />
- )}
- />
- <Route
- path="/public-accounts"
- component={() => <PublicHistoriesPage />}
- />
- <Route
- path="/register"
- component={() => (
- <RegistrationPage
- onComplete={() => {
- route("/account");
- }}
- />
- )}
- />
- <Route
- path="/account"
- component={() => (
- <HomePage
- onPendingOperationFound={(wopid) => {
- route(`/operation/${wopid}`);
- }}
- onRegister={() => {
- route("/register");
- }}
- />
- )}
- />
- <Route
- path="/business"
- component={() => (
- <BusinessAccount
- onClose={() => {
- route("/account");
- }}
- onRegister={() => {
- route("/register");
- }}
- onLoadNotOk={() => {
- route("/account");
- }}
- />
- )}
- />
- <Route default component={Redirect} to="/account" />
- </Router>
- </BankFrame>
- );
-}
-
-function Redirect({ to }: { to: string }): VNode {
- useEffect(() => {
- route(to, true);
- }, []);
- return <div>being redirected to {to}</div>;
-}
-
-export function assertUnreachable(x: never): never {
- throw new Error("Didn't expect to get here");
-}
diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
new file mode 100644
index 000000000..6acf0361e
--- /dev/null
+++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
@@ -0,0 +1,167 @@
+import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
+import { useState } from "preact/hooks";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { buildRequestErrorMessage } from "../utils.js";
+import { AccountForm } from "./admin/AccountForm.js";
+
+export function ShowAccountDetails({
+ account,
+ onClear,
+ onUpdateSuccess,
+ onLoadNotOk,
+ onChangePassword,
+}: {
+ onLoadNotOk: <T>(
+ error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
+ ) => VNode;
+ onClear?: () => void;
+ onChangePassword: () => void;
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useBusinessAccountDetails(account);
+ const { updateAccount } = useAdminAccountAPI();
+ const [update, setUpdate] = useState(false);
+ const [submitAccount, setSubmitAccount] = useState<
+ SandboxBackend.Circuit.CircuitAccountData | undefined
+ >();
+
+ if (!result.ok) {
+ if (result.loading || result.type === ErrorType.TIMEOUT) {
+ return onLoadNotOk(result);
+ }
+ if (result.status === HttpStatusCode.NotFound) {
+ return <div>account not found</div>;
+ }
+ return onLoadNotOk(result);
+ }
+
+ async function doUpdate() {
+ if (!update) {
+ setUpdate(true);
+ } else {
+ if (!submitAccount) return;
+ try {
+ await updateAccount(account, {
+ cashout_address: submitAccount.cashout_address,
+ contact_data: submitAccount.contact_data,
+ });
+ onUpdateSuccess();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Forbidden
+ ? i18n.str`The rights to change the account are not sufficient`
+ : status === HttpStatusCode.NotFound
+ ? i18n.str`The username was not found`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+ }
+
+ return (
+ <div>
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 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">
+ {update ?
+ <i18n.Translate>Update account</i18n.Translate>
+ :
+ <i18n.Translate>Account details</i18n.Translate>
+ }
+ </h2>
+ <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>change the account details</i18n.Translate>
+ </span>
+ </span>
+ <button type="button" data-enabled={!update} class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 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={() => {
+ setUpdate(!update)
+ }}>
+ <span aria-hidden="true" data-enabled={!update} class="translate-x-5 data-[enabled=true]: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>
+ <AccountForm
+ template={result.data}
+ purpose={update ? "update" : "show"}
+ onChange={(a) => setSubmitAccount(a)}
+ >
+
+ </AccountForm>
+
+ <p class="buttons-account">
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-between",
+ flexFlow: "wrap-reverse",
+ }}
+ >
+ <div>
+ {onClear ? (
+ <input
+ class="pure-button"
+ type="submit"
+ value={i18n.str`Close`}
+ onClick={async (e) => {
+ e.preventDefault();
+ onClear();
+ }}
+ />
+ ) : undefined}
+ </div>
+ <div style={{ display: "flex" }}>
+ <div>
+ <input
+ id="select-exchange"
+ class="pure-button pure-button-primary content"
+ disabled={update && !submitAccount}
+ type="submit"
+ value={i18n.str`Change password`}
+ onClick={async (e) => {
+ e.preventDefault();
+ onChangePassword();
+ }}
+ />
+ </div>
+ <div>
+ <input
+ id="select-exchange"
+ class="pure-button pure-button-primary content"
+ disabled={update && !submitAccount}
+ type="submit"
+ value={update ? i18n.str`Confirm` : i18n.str`Update`}
+ onClick={async (e) => {
+ e.preventDefault();
+ doUpdate()
+ }}
+ />
+ </div>
+ </div>
+ </div>
+ </p>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
new file mode 100644
index 000000000..46f4fe0ef
--- /dev/null
+++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
@@ -0,0 +1,177 @@
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
+import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
+
+export function UpdateAccountPassword({
+ account,
+ onCancel,
+ onUpdateSuccess,
+ onLoadNotOk,
+ focus,
+}: {
+ onLoadNotOk: <T>(
+ error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
+ ) => VNode;
+ onCancel: () => void;
+ focus?: boolean,
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useBusinessAccountDetails(account);
+ const { changePassword } = useAdminAccountAPI();
+ const [password, setPassword] = useState<string | undefined>();
+ const [repeat, setRepeat] = useState<string | undefined>();
+
+ if (!result.ok) {
+ if (result.loading || result.type === ErrorType.TIMEOUT) {
+ return onLoadNotOk(result);
+ }
+ if (result.status === HttpStatusCode.NotFound) {
+ return <div>account not found</div>;
+ }
+ return onLoadNotOk(result);
+ }
+
+ const errors = undefinedIfEmpty({
+ password: !password ? i18n.str`required` : undefined,
+ repeat: !repeat
+ ? i18n.str`required`
+ : password !== repeat
+ ? i18n.str`password doesn't match`
+ : undefined,
+ });
+
+ async function doChangePassword() {
+ if (!!errors || !password) return;
+ try {
+ const r = await changePassword(account, {
+ new_password: password,
+ });
+ onUpdateSuccess();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(buildRequestErrorMessage(i18n, error.cause));
+ } else {
+ notifyError(i18n.str`Operation failed, please report`, (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString)
+ }
+ }
+ }
+
+ return (
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 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 for 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`New password`}
+ </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)
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </div>
+ {/* <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>user </i18n.Translate>
+ </p> */}
+ </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`}
+ </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">
+ {onCancel ?
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ : <div />
+ }
+ <button type="submit"
+ 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>
+
+ );
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 4c4a38e57..da299b1c8 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -19,40 +19,49 @@ import {
Amounts,
HttpStatusCode,
Logger,
+ TranslatedString,
+ WithdrawUriResult,
parseWithdrawUri,
} from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { Ref, VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
+import { forwardRef } from "preact/compat";
import { useEffect, useRef, useState } from "preact/hooks";
import { useAccessAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-import { forwardRef } from "preact/compat";
+import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js";
+import { useSettings } from "../hooks/settings.js";
+import { OperationState } from "./OperationState/index.js";
+import { Attention } from "../components/Attention.js";
const logger = new Logger("WalletWithdrawForm");
-const RefAmount = forwardRef(Amount);
+const RefAmount = forwardRef(InputAmount);
-export function WalletWithdrawForm({
- focus,
- limit,
- onSuccess,
-}: {
+
+function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
limit: AmountJson;
focus?: boolean;
- onSuccess: (operationId: string) => void;
+ goToConfirmOperation: (operationId: string) => void;
+ onCancel: () => void;
}): VNode {
const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+
const { createWithdrawal } = useAccessAPI();
+ const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`);
- const [amountStr, setAmountStr] = useState<string | undefined>("5.00");
- const ref = useRef<HTMLInputElement>(null);
- useEffect(() => {
- if (focus) ref.current?.focus();
- }, [focus]);
+ if (!!settings.currentWithdrawalOperationId) {
+ return <Attention type="warning" title={i18n.str`There is an operation already`}>
+ <i18n.Translate>
+ To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a>
+ </i18n.Translate>
+ </Attention>
+ }
const trimmedAmountStr = amountStr?.trim();
@@ -65,142 +74,186 @@ export function WalletWithdrawForm({
trimmedAmountStr == null
? i18n.str`required`
: !parsedAmount
- ? i18n.str`invalid`
- : Amounts.cmp(limit, parsedAmount) === -1
- ? i18n.str`balance is not enough`
- : undefined,
+ ? i18n.str`invalid`
+ : Amounts.cmp(limit, parsedAmount) === -1
+ ? i18n.str`balance is not enough`
+ : undefined,
});
- return (
- <form
- id="reserve-form"
- class="pure-form"
- name="tform"
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <p>
- <label for="withdraw-amount">{i18n.str`Amount to withdraw:`}</label>
- &nbsp;
- <RefAmount
- currency={limit.currency}
- value={amountStr}
- onChange={(v) => {
- setAmountStr(v);
- }}
- error={errors?.amount}
- ref={ref}
- />
- </p>
- <p>
- <div>
- <input
- id="select-exchange"
- class="pure-button pure-button-primary"
- type="submit"
- disabled={!!errors}
- value={i18n.str`Withdraw`}
- onClick={async (e) => {
- e.preventDefault();
- if (!parsedAmount) return;
- try {
- const result = await createWithdrawal({
- amount: Amounts.stringify(parsedAmount),
- });
- const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
- if (!uri) {
- return notifyError({
- title: i18n.str`Server responded with an invalid withdraw URI`,
- description: i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`,
- });
- } else {
- onSuccess(uri.withdrawalOperationId);
- }
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Forbidden
- ? i18n.str`The operation was rejected due to insufficient funds`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
+ async function doStart() {
+ if (!parsedAmount) return;
+ try {
+ const result = await createWithdrawal({
+ amount: Amounts.stringify(parsedAmount),
+ });
+ const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
+ if (!uri) {
+ return notifyError(
+ i18n.str`Server responded with an invalid withdraw URI`,
+ i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
+ } else {
+ updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId)
+ goToConfirmOperation(uri.withdrawalOperationId);
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Forbidden
+ ? i18n.str`The operation was rejected due to insufficient funds`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
+ 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()
+ }}
+ >
+ <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>
- </p>
- </form>
- );
+ </div>
+ <div class="mt-4">
+ <div class="sm:inline">
+
+ <button type="button"
+ 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"
+ 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"
+ 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"
+ 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">
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate></button>
+ <button type="submit"
+ 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 Amount(
- {
- currency,
- value,
- error,
- onChange,
- }: {
- error?: string;
- currency: string;
- value: string | undefined;
- onChange?: (s: string) => void;
- },
- ref: Ref<HTMLInputElement>,
-): VNode {
- return (
- <div style={{ width: "max-content" }}>
- <div>
- <input
- type="text"
- readonly
- class="currency-indicator"
- size={currency.length}
- maxLength={currency.length}
- tabIndex={-1}
- style={{
- borderTopRightRadius: 0,
- borderBottomRightRadius: 0,
- borderRight: 0,
- }}
- value={currency}
+
+export function WalletWithdrawForm({
+ focus,
+ limit,
+ onCancel,
+ goToConfirmOperation,
+}: {
+ limit: AmountJson;
+ focus?: boolean;
+ goToConfirmOperation: (operationId: string) => void;
+ onCancel: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+
+ return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 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 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 GNU Taler Wallet`} onClose={() => {
+ updateSettings("showInstallWallet", false);
+ }}>
+ <i18n.Translate>
+ If you don't have one yet you can follow the instruction <a target="_blank" rel="noreferrer noopener" class="font-semibold text-blue-700 hover:text-blue-600" href="https://taler.net/en/wallet.html">here</a>
+ </i18n.Translate>
+ </Attention>
+ }
+
+ {!settings.fastWithdrawal ?
+ <OldWithdrawalForm
+ focus={focus}
+ limit={limit}
+ onCancel={onCancel}
+ goToConfirmOperation={goToConfirmOperation}
/>
- <input
- type="number"
- ref={ref}
- name="amount"
- id="amount"
- placeholder="0"
- style={{
- borderTopLeftRadius: 0,
- borderBottomLeftRadius: 0,
- borderLeft: 0,
- width: 150,
- color: "black",
- }}
- value={value ?? ""}
- disabled={!onChange}
- onInput={(e): void => {
- if (onChange) {
- onChange(e.currentTarget.value);
- }
- }}
+ :
+ <OperationState
+ currency={limit.currency}
+ onClose={onCancel}
/>
- </div>
- <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+ }
</div>
+ </div>
);
}
+
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index cdb612155..ddcd2492d 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -15,26 +15,41 @@
*/
import {
+ AmountJson,
+ Amounts,
HttpStatusCode,
Logger,
- WithdrawUriResult,
+ PaytoUri,
+ PaytoUriIBAN,
+ PaytoUriTalerBank,
+ TranslatedString,
+ WithdrawUriResult
} from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
+ notifyInfo,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useMemo, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAccessAnonAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { useSettings } from "../hooks/settings.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
const logger = new Logger("WithdrawalConfirmationQuestion");
interface Props {
onAborted: () => void;
withdrawUri: WithdrawUriResult;
+ details: {
+ account: PaytoUri,
+ reserve: string,
+ amount: AmountJson,
+ }
}
/**
* Additional authentication required to complete the operation.
@@ -42,9 +57,11 @@ interface Props {
*/
export function WithdrawalConfirmationQuestion({
onAborted,
+ details,
withdrawUri,
}: Props): VNode {
const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
const captchaNumbers = useMemo(() => {
return {
@@ -56,139 +73,263 @@ export function WithdrawalConfirmationQuestion({
const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI();
const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
const answer = parseInt(captchaAnswer ?? "", 10);
+ const [busy, setBusy] = useState<Record<string, undefined>>()
const errors = undefinedIfEmpty({
answer: !captchaAnswer
? i18n.str`Answer the question before continue`
: Number.isNaN(answer)
- ? i18n.str`The answer should be a number`
- : answer !== captchaNumbers.a + captchaNumbers.b
- ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
- : undefined,
- });
+ ? i18n.str`The answer should be a number`
+ : answer !== captchaNumbers.a + captchaNumbers.b
+ ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
+ : undefined,
+ }) ?? busy;
+
+ async function doTransfer() {
+ try {
+ setBusy({})
+ await confirmWithdrawal(
+ withdrawUri.withdrawalOperationId,
+ );
+ if (!settings.showWithdrawalSuccess) {
+ notifyInfo(i18n.str`Wire transfer completed!`)
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
+ : status === HttpStatusCode.UnprocessableEntity
+ ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+
+ async function doCancel() {
+ try {
+ setBusy({})
+ await abortWithdrawal(withdrawUri.withdrawalOperationId);
+ onAborted();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+
return (
<Fragment>
- <h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
- <article>
- <div class="challenge-div">
- <form
- class="challenge-form"
- noValidate
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <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"
- value={captchaAnswer ?? ""}
- type="text"
- autoFocus
- required
- onInput={(e): void => {
- setCaptchaAnswer(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.answer}
- isDirty={captchaAnswer !== undefined}
- />
- </p>
- <p>
- <button
- type="submit"
- class="pure-button pure-button-primary btn-confirm"
- disabled={!!errors}
- onClick={async (e) => {
- e.preventDefault();
- try {
- await confirmWithdrawal(
- withdrawUri.withdrawalOperationId,
- );
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Conflict
- ? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
- : status === HttpStatusCode.UnprocessableEntity
- ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ <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-2 max-w-xl text-sm text-gray-500">
+ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-3 sm:gap-x-3">
+
+ <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-noneborder-indigo-600 ring-2 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" />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>challenge response test</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="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>
+ </label>
+
+
+ <label class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none 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" />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>using SMS</i18n.Translate>
+ </span>
+ <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>not available</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="h-5 w-5 text-indigo-600 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 class="relative flex cursor-pointer rounded-lg border bg-gray-100 p-4 shadow-sm focus:outline-none 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" />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>one time password</i18n.Translate>
+ </span>
+ <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>not available</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ <svg class="h-5 w-5 text-indigo-600 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 class="mt-3 text-sm leading-6">
+
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 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 text-gray-900"><i18n.Translate>Answer the next question to authorize the wire transfer.</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">
+ <label for="withdraw-amount">{i18n.str`What is`}&nbsp;
+ <em>
+ {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
+ </em>
+ ?
+ </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={captchaAnswer ?? ""}
+ required
+
+ name="answer"
+ id="answer"
+ autocomplete="off"
+ onChange={(e): void => {
+ setCaptchaAnswer(e.currentTarget.value)
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== undefined} />
+ </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" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={doCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate></button>
+ <button type="submit"
+ 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()
+ doTransfer()
+ }}
+ >
+ <i18n.Translate>Transfer</i18n.Translate>
+ </button>
+ </div>
+
+ </form>
+ </div>
+ </div>
+ <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 => {
+ switch (details.account.targetType) {
+ case "iban": {
+ const p = details.account as PaytoUriIBAN
+ const name = p.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">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.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">Exchange name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ </div>
+ }
+ </Fragment>
}
- }
- }}
- >
- {i18n.str`Confirm`}
- </button>
- &nbsp;
- <button
- class="pure-button pure-button-secondary btn-cancel"
- onClick={async (e) => {
- e.preventDefault();
- try {
- await abortWithdrawal(withdrawUri.withdrawalOperationId);
- onAborted();
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Conflict
- ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ case "x-taler-bank": {
+ const p = details.account as PaytoUriTalerBank
+ const name = p.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">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.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">Exchange name</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ </div>
+ }
+ </Fragment>
}
+ default:
+ 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">Exchange account</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
+ </div>
+
}
- }}
- >
- {i18n.str`Cancel`}
- </button>
- </p>
+ })()}
+ <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={details.amount} />
+ </dd>
+ </div>
+ </dl>
+ </div>
</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>
+ </div>
+
</Fragment>
);
}
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 80fdac3c8..91c5da718 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -15,15 +15,16 @@
*/
import {
+ Amounts,
HttpStatusCode,
Logger,
WithdrawUriResult,
+ parsePaytoUri
} from "@gnu-taler/taler-util";
-import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ErrorType, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { Loading } from "../components/Loading.js";
import { useWithdrawalDetails } from "../hooks/access.js";
-import { notifyInfo } from "../hooks/notification.js";
import { useSettings } from "../hooks/settings.js";
import { handleNotOkResult } from "./HomePage.js";
import { QrCodeSection } from "./QrCodeSection.js";
@@ -33,8 +34,7 @@ const logger = new Logger("WithdrawalQRCode");
interface Props {
withdrawUri: WithdrawUriResult;
- onContinue: () => void;
- onLoadNotOk: () => void;
+ onClose: () => void;
}
/**
* Offer the QR code (and a clickable taler://-link) to
@@ -43,27 +43,15 @@ interface Props {
*/
export function WithdrawalQRCode({
withdrawUri,
- onContinue,
- onLoadNotOk,
+ onClose,
}: Props): VNode {
- const [settings, updateSettings] = useSettings();
- function clearCurrentWithdrawal(): void {
- updateSettings("currentWithdrawalOperationId", undefined);
- onContinue();
- }
const { i18n } = useTranslationContext();
const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
+
if (!result.ok) {
if (result.loading) {
return <Loading />;
}
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- ) {
- return <div>operation not found</div>;
- }
- onLoadNotOk();
return handleNotOkResult(i18n)(result);
}
const { data } = result;
@@ -84,12 +72,11 @@ export function WithdrawalQRCode({
</i18n.Translate>
</p>
<a class="pure-button pure-button-primary"
- style={{float:"right"}}
+ style={{ float: "right" }}
onClick={async (e) => {
e.preventDefault();
- clearCurrentWithdrawal()
- onContinue()
- }}>
+ onClose()
+ }}>
{i18n.str`Continue`}
</a>
@@ -98,57 +85,77 @@ export function WithdrawalQRCode({
}
if (data.confirmation_done) {
- return <section id="main" class="content">
- <h1 class="nav">{i18n.str`Operation completed`}</h1>
+ 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">
+ <button type="button"
+ 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"
+ onClick={async (e) => {
+ e.preventDefault();
+ onClose()
+ }}>
+ <i18n.Translate>Done</i18n.Translate>
+ </button>
+ </div>
+ </div>
- <section id="assets" style={{maxWidth: 400, marginLeft: "auto", marginRight:"auto"}}>
- <p>
- <i18n.Translate>
- The wire transfer to the GNU Taler Exchange bank's account is completed, now the
- exchange will send the requested amount into your GNU Taler wallet.
- </i18n.Translate>
- </p>
- <p>
- <i18n.Translate>
- You can close this page now or continue to the account page.
- </i18n.Translate>
- </p>
- <div style={{textAlign:"center"}}>
- <a class="pure-button pure-button-primary"
- onClick={async (e) => {
- e.preventDefault();
- clearCurrentWithdrawal()
- onContinue()
- }}>
- {i18n.str`Continue`}
- </a>
- </div>
- </section>
- </section>
}
-
if (!data.selection_done) {
return (
<QrCodeSection
withdrawUri={withdrawUri}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
- clearCurrentWithdrawal()
- onContinue()
- }}
+ onClose()
+ }}
/>
);
}
+ if (!data.selected_reserve_pub) {
+ return <div>
+ the exchange is selcted but no reserve pub
+ </div>
+ }
+
+ const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account)
+
+ if (!account) {
+ return <div>
+ the exchange is selcted but no account
+ </div>
+ }
return (
<WithdrawalConfirmationQuestion
withdrawUri={withdrawUri}
+ details={{
+ account,
+ reserve: data.selected_reserve_pub,
+ amount: Amounts.parseOrThrow(data.amount)
+ }}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
- clearCurrentWithdrawal()
- onContinue()
- }}
+ onClose()
+ }}
/>
);
-} \ No newline at end of file
+}
diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx b/packages/demobank-ui/src/pages/admin/Account.tsx
new file mode 100644
index 000000000..676fc43d0
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/Account.tsx
@@ -0,0 +1,38 @@
+import { Amounts } from "@gnu-taler/taler-util";
+import { PaytoWireTransferForm } from "../PaytoWireTransferForm.js";
+import { handleNotOkResult } from "../HomePage.js";
+import { useAccountDetails } from "../../hooks/access.js";
+import { useBackendContext } from "../../context/backend.js";
+import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+
+export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
+ const { i18n } = useTranslationContext();
+ const r = useBackendContext();
+ const account = r.state.status !== "loggedOut" ? r.state.username : "admin";
+ const result = useAccountDetails(account);
+
+ if (!result.ok) {
+ return handleNotOkResult(i18n)(result);
+ }
+ const { data } = result;
+
+ const balance = Amounts.parseOrThrow(data.balance.amount);
+ const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
+
+ const debitThreshold = Amounts.parseOrThrow(result.data.debit_threshold);
+ const limit = balanceIsDebit
+ ? Amounts.sub(debitThreshold, balance).amount
+ : Amounts.add(balance, debitThreshold).amount;
+ if (!balance) return <Fragment />;
+ return (
+ <PaytoWireTransferForm
+ title={i18n.str`Make a wire transfer`}
+ limit={limit}
+ onSuccess={() => {
+ notifyInfo(i18n.str`Wire transfer created!`);
+ }}
+ onCancel={undefined}
+ />
+ );
+}
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
new file mode 100644
index 000000000..ed8bf610d
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -0,0 +1,315 @@
+import { ComponentChildren, VNode, h } from "preact";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { buildPayto, parsePaytoUri } from "@gnu-taler/taler-util";
+import { doAutoFocus } from "../PaytoWireTransferForm.js";
+
+const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
+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 ]*$/;
+
+/**
+ * Create valid account object to update or create
+ * Take template as initial values for the form
+ * Purpose indicate if all field al read only (show), part of them (update)
+ * or none (create)
+ * @param param0
+ * @returns
+ */
+export function AccountForm({
+ template,
+ purpose,
+ onChange,
+ focus,
+ children,
+}: {
+ focus?: boolean,
+ children: ComponentChildren,
+ template: SandboxBackend.Circuit.CircuitAccountData | undefined;
+ onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
+ purpose: "create" | "update" | "show";
+}): VNode {
+ const initial = initializeFromTemplate(template);
+ const [form, setForm] = useState(initial);
+ const [errors, setErrors] = useState<
+ RecursivePartial<typeof initial> | undefined
+ >(undefined);
+ const { i18n } = useTranslationContext();
+
+ function updateForm(newForm: typeof initial): void {
+
+ const parsed = !newForm.cashout_address
+ ? undefined
+ : buildPayto("iban", newForm.cashout_address, undefined);;
+
+ const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
+ cashout_address: !newForm.cashout_address
+ ? i18n.str`required`
+ : !parsed
+ ? i18n.str`does not follow the pattern`
+ : !parsed.isKnown || parsed.targetType !== "iban"
+ ? i18n.str`only "IBAN" target are supported`
+ : !IBAN_REGEX.test(parsed.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : validateIBAN(parsed.iban, i18n),
+ contact_data: undefinedIfEmpty({
+ email: !newForm.contact_data?.email
+ ? i18n.str`required`
+ : !EMAIL_REGEX.test(newForm.contact_data.email)
+ ? i18n.str`it should be an email`
+ : undefined,
+ phone: !newForm.contact_data?.phone
+ ? i18n.str`required`
+ : !newForm.contact_data.phone.startsWith("+")
+ ? i18n.str`should start with +`
+ : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
+ ? i18n.str`phone number can't have other than numbers`
+ : undefined,
+ }),
+ // iban: !newForm.iban
+ // ? undefined //optional field
+ // : !IBAN_REGEX.test(newForm.iban)
+ // ? i18n.str`IBAN should have just uppercased letters and numbers`
+ // : validateIBAN(newForm.iban, i18n),
+ name: !newForm.name ? i18n.str`required` : undefined,
+ username: !newForm.username ? i18n.str`required` : undefined,
+ });
+ setErrors(errors);
+ setForm(newForm);
+ onChange(errors === undefined ? (newForm as any) : undefined);
+ }
+
+ 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`Username`}
+ {purpose === "create" && <b style={{ color: "red" }}> *</b>}
+ </label>
+ <div class="mt-2">
+ <input
+ ref={focus ? 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={purpose !== "create"}
+ value={form.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 identification in the bank</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`Name`}
+ {purpose === "create" && <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={purpose !== "create"}
+ value={form.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 person owner the account</i18n.Translate>
+ </p>
+ </div>
+
+
+ {purpose !== "create" && (<div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="internal-iban"
+ >
+ {i18n.str`Internal 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="internal-iban"
+ id="internal-iban"
+ disabled={true}
+ value={form.iban ?? ""}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>international bank account number</i18n.Translate>
+ </p>
+ </div>)}
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="email"
+ >
+ {i18n.str`Email`}
+ {purpose === "create" && <b style={{ color: "red" }}> *</b>}
+ </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?.contact_data?.email && form.contact_data.email !== undefined}
+ disabled={purpose !== "create"}
+ value={form.contact_data.email ?? ""}
+ onChange={(e) => {
+ form.contact_data.email = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.contact_data?.email}
+ isDirty={form.contact_data.email !== undefined}
+ />
+ </div>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="phone"
+ >
+ {i18n.str`Phone`}
+ {purpose === "create" && <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="phone"
+ id="phone"
+ disabled={purpose !== "create"}
+ value={form.contact_data.phone ?? ""}
+ data-error={!!errors?.contact_data?.phone && form.contact_data.phone !== undefined}
+ onChange={(e) => {
+ form.contact_data.phone = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.contact_data?.phone}
+ isDirty={form.contact_data.phone !== undefined}
+ />
+ </div>
+ </div>
+
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="cashout"
+ >
+ {i18n.str`Cashout IBAN`}
+ {purpose !== "show" && <b style={{ color: "red" }}> *</b>}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ data-error={!!errors?.cashout_address && form.cashout_address !== 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_address ?? ""}
+ onChange={(e) => {
+ form.cashout_address = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.cashout_address}
+ isDirty={form.cashout_address !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate>account number where the money is going to be sent when doing cashouts</i18n.Translate>
+ </p>
+ </div>
+
+ </div>
+ </div>
+ {children}
+ </form>
+ );
+}
+
+function initializeFromTemplate(
+ account: SandboxBackend.Circuit.CircuitAccountData | undefined,
+): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
+ const emptyAccount = {
+ cashout_address: undefined,
+ iban: undefined,
+ name: undefined,
+ username: undefined,
+ contact_data: undefined,
+ };
+ const emptyContact = {
+ email: undefined,
+ phone: undefined,
+ };
+
+ const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
+ structuredClone(account) ?? emptyAccount;
+ if (typeof initial.contact_data === "undefined") {
+ initial.contact_data = emptyContact;
+ }
+ initial.contact_data.email;
+ return initial as any;
+}
+
+
diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx
new file mode 100644
index 000000000..a6899e679
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx
@@ -0,0 +1,132 @@
+import { h, VNode } from "preact";
+import { useBusinessAccounts } from "../../hooks/circuit.js";
+import { handleNotOkResult } from "../HomePage.js";
+import { AccountAction } from "./Home.js";
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { RenderAmount } from "../PaytoWireTransferForm.js";
+
+interface Props {
+ onAction: (type: AccountAction, account: string) => void;
+ account: string | undefined;
+ onCreateAccount: () => void;
+}
+
+export function AccountList({ account, onAction, onCreateAccount }: Props): VNode {
+ const result = useBusinessAccounts({ account });
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <div />;
+ if (!result.ok) {
+ return handleNotOkResult(i18n)(result);
+ }
+
+ const { customers } = result.data;
+ return <div class="px-4 sm:px-6 lg:px-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>
+ <p class="mt-2 text-sm text-gray-700">
+ <i18n.Translate>A list of all business account in the bank.</i18n.Translate>
+ </p>
+ </div>
+ <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
+ <button 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"
+ onClick={(e) => {
+ e.preventDefault()
+ onCreateAccount()
+ }}>
+ <i18n.Translate>Create account</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ <div class="mt-8 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">
+ {!customers.length ? (
+ <div></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">
+ {customers.map((item, idx) => {
+ const balance = !item.balance
+ ? undefined
+ : Amounts.parse(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 href="#" class="text-indigo-600 hover:text-indigo-900"
+ onClick={(e) => {
+ e.preventDefault();
+ onAction("show-details", item.username)
+ }}
+ >
+ {item.username}
+ </a>
+
+
+ </td>
+ <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
+ {item.name}
+ </td>
+ <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
+ {!balance ? (
+ i18n.str`unknown`
+ ) : (
+ <span class="amount">
+ <RenderAmount value={balance} negative={balanceIsDebit} />
+ </span>
+ )}
+ </td>
+ <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
+ <a href="#" class="text-indigo-600 hover:text-indigo-900"
+ onClick={(e) => {
+ e.preventDefault();
+ onAction("update-password", item.username)
+ }}
+ >
+ change password
+ </a>
+ <br />
+ <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
+ e.preventDefault();
+ onAction("show-cashout", item.username)
+ }}
+ >
+ cashouts
+ </a>
+ <br />
+ <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
+ e.preventDefault();
+ onAction("remove-account", item.username)
+ }}
+ >
+ remove
+ </a>
+ </td>
+ </tr>
+ })}
+
+ </tbody>
+ </table>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
new file mode 100644
index 000000000..2146fc6f0
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -0,0 +1,101 @@
+import { RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h, Fragment } from "preact";
+import { useAdminAccountAPI } from "../../hooks/circuit.js";
+import { useState } from "preact/hooks";
+import { buildRequestErrorMessage } from "../../utils.js";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { getRandomPassword } from "../rnd.js";
+import { AccountForm } from "./AccountForm.js";
+
+export function CreateNewAccount({
+ onCancel,
+ onCreateSuccess,
+}: {
+ onCancel: () => void;
+ onCreateSuccess: (password: string) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { createAccount } = useAdminAccountAPI();
+ const [submitAccount, setSubmitAccount] = useState<
+ SandboxBackend.Circuit.CircuitAccountData | undefined
+ >();
+
+ async function doCreate() {
+ if (!submitAccount) return;
+ try {
+ const account: SandboxBackend.Circuit.CircuitAccountRequest =
+ {
+ cashout_address: submitAccount.cashout_address,
+ contact_data: submitAccount.contact_data,
+ internal_iban: submitAccount.iban,
+ name: submitAccount.name,
+ username: submitAccount.username,
+ password: getRandomPassword(),
+ };
+
+ await createAccount(account);
+ onCreateSuccess(account.password);
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Forbidden
+ ? i18n.str`The rights to perform the operation are not sufficient`
+ : status === HttpStatusCode.BadRequest
+ ? i18n.str`Server replied that input data was invalid`
+ : status === HttpStatusCode.Conflict
+ ? i18n.str`At least one registration detail was not available`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
+ return (
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 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>New business 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">
+ {onCancel ?
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ : <div />
+ }
+ <button type="submit"
+ 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/demobank-ui/src/pages/admin/Home.tsx b/packages/demobank-ui/src/pages/admin/Home.tsx
new file mode 100644
index 000000000..d50ff14b4
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/Home.tsx
@@ -0,0 +1,148 @@
+import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Cashouts } from "../../components/Cashouts/index.js";
+import { ShowCashoutDetails } from "../business/Home.js";
+import { handleNotOkResult } from "../HomePage.js";
+import { ShowAccountDetails } from "../ShowAccountDetails.js";
+import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
+import { AdminAccount } from "./Account.js";
+import { AccountList } from "./AccountList.js";
+import { CreateNewAccount } from "./CreateNewAccount.js";
+import { RemoveAccount } from "./RemoveAccount.js";
+import { Transactions } from "../../components/Transactions/index.js";
+
+/**
+ * Query account information and show QR code if there is pending withdrawal
+ */
+interface Props {
+ onRegister: () => void;
+}
+export type AccountAction = "show-details" |
+ "show-cashout" |
+ "update-password" |
+ "remove-account" |
+ "show-cashouts-details";
+
+export function AdminHome({ onRegister }: Props): VNode {
+ const [action, setAction] = useState<{
+ type: AccountAction,
+ account: string
+ } | undefined>()
+
+ const [createAccount, setCreateAccount] = useState(false);
+
+ const { i18n } = useTranslationContext();
+
+ if (action) {
+ switch (action.type) {
+ case "show-cashouts-details": return <ShowCashoutDetails
+ id={action.account}
+ onLoadNotOk={handleNotOkResult(i18n)}
+ onCancel={() => {
+ setAction(undefined);
+ }}
+ />
+ case "show-cashout": return (
+ <div>
+ <div>
+ <h1 class="nav welcome-text">
+ <i18n.Translate>Cashout for account {action.account}</i18n.Translate>
+ </h1>
+ </div>
+ <Cashouts
+ account={action.account}
+ onSelected={(id) => {
+ setAction({
+ type: "show-cashouts-details",
+ account: action.account
+ });
+ }}
+ />
+ <p>
+ <input
+ class="pure-button"
+ type="submit"
+ value={i18n.str`Close`}
+ onClick={async (e) => {
+ e.preventDefault();
+ setAction(undefined);
+ }}
+ />
+ </p>
+ </div>
+ )
+ case "update-password": return <UpdateAccountPassword
+ account={action.account}
+ onLoadNotOk={handleNotOkResult(i18n)}
+ onUpdateSuccess={() => {
+ notifyInfo(i18n.str`Password changed`);
+ setAction(undefined);
+ }}
+ onCancel={() => {
+ setAction(undefined);
+ }}
+ />
+ case "remove-account": return <RemoveAccount
+ account={action.account}
+ onLoadNotOk={handleNotOkResult(i18n)}
+ onUpdateSuccess={() => {
+ notifyInfo(i18n.str`Account removed`);
+ setAction(undefined);
+ }}
+ onCancel={() => {
+ setAction(undefined);
+ }}
+ />
+ case "show-details": return <ShowAccountDetails
+ account={action.account}
+ onLoadNotOk={handleNotOkResult(i18n)}
+ onChangePassword={() => {
+ setAction({
+ type: "update-password",
+ account: action.account,
+ })
+ }}
+ onUpdateSuccess={() => {
+ notifyInfo(i18n.str`Account updated`);
+ setAction(undefined);
+ }}
+ onClear={() => {
+ setAction(undefined);
+ }}
+ />
+ }
+ }
+
+ if (createAccount) {
+ return (
+ <CreateNewAccount
+ onCancel={() => setCreateAccount(false)}
+ onCreateSuccess={(password) => {
+ notifyInfo(
+ i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
+ );
+ setCreateAccount(false);
+ }}
+ />
+ );
+ }
+
+ return (
+ <Fragment>
+
+ <AccountList
+ onCreateAccount={() => {
+ setCreateAccount(true);
+ }}
+ account={undefined}
+ onAction={(type, account) => setAction({ account, type })}
+
+ />
+
+ <AdminAccount onRegister={onRegister} />
+
+ <Transactions account="admin"/>
+ </Fragment>
+ );
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
new file mode 100644
index 000000000..b323b0d01
--- /dev/null
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -0,0 +1,171 @@
+import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h, Fragment } from "preact";
+import { useAccountDetails } from "../../hooks/access.js";
+import { useAdminAccountAPI } from "../../hooks/circuit.js";
+import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { Attention } from "../../components/Attention.js";
+import { doAutoFocus } from "../PaytoWireTransferForm.js";
+
+export function RemoveAccount({
+ account,
+ onCancel,
+ onUpdateSuccess,
+ onLoadNotOk,
+ focus,
+}: {
+ onLoadNotOk: <T>(
+ error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
+ ) => VNode;
+ focus?: boolean;
+ onCancel: () => void;
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useAccountDetails(account);
+ const [accountName, setAccountName] = useState<string | undefined>()
+ const { deleteAccount } = useAdminAccountAPI();
+
+ if (!result.ok) {
+ if (result.loading || result.type === ErrorType.TIMEOUT) {
+ return onLoadNotOk(result);
+ }
+ if (result.status === HttpStatusCode.NotFound) {
+ return <div>account not found</div>;
+ }
+ return onLoadNotOk(result);
+ }
+ const balance = Amounts.parse(result.data.balance.amount);
+ if (!balance) {
+ return <div>there was an error reading the balance</div>;
+ }
+ const isBalanceEmpty = Amounts.isZero(balance);
+ if (!isBalanceEmpty) {
+ return <Attention type="warning" title={i18n.str`Can't delete the account`} onClose={onCancel}>
+ <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>
+ }
+
+ async function doRemove() {
+ try {
+ const r = await deleteAccount(account);
+ onUpdateSuccess();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Forbidden
+ ? i18n.str`The administrator specified a institutional username`
+ : status === HttpStatusCode.NotFound
+ ? i18n.str`The username was not found`
+ : status === HttpStatusCode.PreconditionFailed
+ ? i18n.str`Balance was not zero`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString);
+ }
+ }
+ }
+
+ const errors = undefinedIfEmpty({
+ accountName: !accountName
+ ? i18n.str`required`
+ : account !== accountName
+ ? i18n.str`name doesn't match`
+ : undefined,
+ });
+
+
+ return (
+ <div>
+ <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-10 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">
+ {onCancel ?
+ <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ : <div />
+ }
+ <button type="submit"
+ 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/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/business/Home.tsx
index d9aa8fa36..1a84effcd 100644
--- a/packages/demobank-ui/src/pages/BusinessAccount.tsx
+++ b/packages/demobank-ui/src/pages/business/Home.tsx
@@ -17,65 +17,63 @@ import {
AmountJson,
Amounts,
HttpStatusCode,
- TranslatedString,
+ TranslatedString
} from "@gnu-taler/taler-util";
import {
HttpResponse,
HttpResponsePaginated,
RequestError,
+ notify,
+ notifyError,
+ notifyInfo,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { StateUpdater, useEffect, useState } from "preact/hooks";
-import { Cashouts } from "../components/Cashouts/index.js";
-import { useBackendContext } from "../context/backend.js";
-import { useAccountDetails } from "../hooks/access.js";
+import { useEffect, useState } from "preact/hooks";
+import { Cashouts } from "../../components/Cashouts/index.js";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { useAccountDetails } from "../../hooks/access.js";
import {
useCashoutDetails,
useCircuitAccountAPI,
useEstimator,
useRatiosAndFeeConfig,
-} from "../hooks/circuit.js";
+} from "../../hooks/circuit.js";
import {
TanChannel,
buildRequestErrorMessage,
undefinedIfEmpty,
-} from "../utils.js";
-import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
-import { ErrorBannerFloat } from "./BankFrame.js";
-import { LoginForm } from "./LoginForm.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-import { handleNotOkResult } from "./HomePage.js";
-import { ErrorMessage, notifyInfo } from "../hooks/notification.js";
-import { Amount } from "./WalletWithdrawForm.js";
+} from "../../utils.js";
+import { handleNotOkResult } from "../HomePage.js";
+import { InputAmount } from "../PaytoWireTransferForm.js";
+import { ShowAccountDetails } from "../ShowAccountDetails.js";
+import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
interface Props {
+ account: string,
onClose: () => void;
onRegister: () => void;
onLoadNotOk: () => void;
}
export function BusinessAccount({
onClose,
+ account,
onLoadNotOk,
onRegister,
}: Props): VNode {
const { i18n } = useTranslationContext();
- const backend = useBackendContext();
const [updatePassword, setUpdatePassword] = useState(false);
const [newCashout, setNewcashout] = useState(false);
const [showCashoutDetails, setShowCashoutDetails] = useState<
string | undefined
>();
- if (backend.state.status === "loggedOut") {
- return <LoginForm onRegister={onRegister} />;
- }
if (newCashout) {
return (
<CreateCashout
- account={backend.state.username}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+ account={account}
+ onLoadNotOk={handleNotOkResult(i18n)}
onCancel={() => {
setNewcashout(false);
}}
@@ -93,7 +91,7 @@ export function BusinessAccount({
return (
<ShowCashoutDetails
id={showCashoutDetails}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+ onLoadNotOk={handleNotOkResult(i18n)}
onCancel={() => {
setShowCashoutDetails(undefined);
}}
@@ -103,13 +101,13 @@ export function BusinessAccount({
if (updatePassword) {
return (
<UpdateAccountPassword
- account={backend.state.username}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+ account={account}
+ onLoadNotOk={handleNotOkResult(i18n)}
onUpdateSuccess={() => {
notifyInfo(i18n.str`Password changed`);
setUpdatePassword(false);
}}
- onClear={() => {
+ onCancel={() => {
setUpdatePassword(false);
}}
/>
@@ -118,8 +116,8 @@ export function BusinessAccount({
return (
<div>
<ShowAccountDetails
- account={backend.state.username}
- onLoadNotOk={handleNotOkResult(i18n, onRegister)}
+ account={account}
+ onLoadNotOk={handleNotOkResult(i18n)}
onUpdateSuccess={() => {
notifyInfo(i18n.str`Account updated`);
}}
@@ -132,7 +130,7 @@ export function BusinessAccount({
<div class="active">
<h3>{i18n.str`Latest cashouts`}</h3>
<Cashouts
- account={backend.state.username}
+ account={account}
onSelected={(id) => {
setShowCashoutDetails(id);
}}
@@ -201,13 +199,13 @@ function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse<
(result.data.name !== oldResult.name ||
result.data.version !== oldResult.version ||
result.data.ratios_and_fees.buy_at_ratio !==
- oldResult.ratios_and_fees.buy_at_ratio ||
+ oldResult.ratios_and_fees.buy_at_ratio ||
result.data.ratios_and_fees.buy_in_fee !==
- oldResult.ratios_and_fees.buy_in_fee ||
+ oldResult.ratios_and_fees.buy_in_fee ||
result.data.ratios_and_fees.sell_at_ratio !==
- oldResult.ratios_and_fees.sell_at_ratio ||
+ oldResult.ratios_and_fees.sell_at_ratio ||
result.data.ratios_and_fees.sell_out_fee !==
- oldResult.ratios_and_fees.sell_out_fee ||
+ oldResult.ratios_and_fees.sell_out_fee ||
result.data.fiat_currency !== oldResult.fiat_currency);
return {
@@ -225,7 +223,6 @@ function CreateCashout({
const { i18n } = useTranslationContext();
const ratiosResult = useRatiosAndFeeConfig();
const result = useAccountDetails(account);
- const [error, saveError] = useState<ErrorMessage | undefined>();
const {
estimateByCredit: calculateFromCredit,
estimateByDebit: calculateFromDebit,
@@ -238,9 +235,10 @@ function CreateCashout({
const config = ratiosResult.data;
const balance = Amounts.parseOrThrow(result.data.balance.amount);
- const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
- const zero = Amounts.zeroOfCurrency(balance.currency);
const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
+
+ const debitThreshold = Amounts.parseOrThrow(result.data.debit_threshold);
+ const zero = Amounts.zeroOfCurrency(balance.currency);
const limit = balanceIsDebit
? Amounts.sub(debitThreshold, balance).amount
: Amounts.add(balance, debitThreshold).amount;
@@ -251,15 +249,14 @@ function CreateCashout({
const sellFee = !config.ratios_and_fees.sell_out_fee
? zero
: Amounts.parseOrThrow(
- `${balance.currency}:${config.ratios_and_fees.sell_out_fee}`,
- );
+ `${balance.currency}:${config.ratios_and_fees.sell_out_fee}`,
+ );
const fiatCurrency = config.fiat_currency;
if (!sellRate || sellRate < 0) return <div>error rate</div>;
const amount = Amounts.parseOrThrow(
- `${!form.isDebit ? fiatCurrency : balance.currency}:${
- !form.amount ? "0" : form.amount
+ `${!form.isDebit ? fiatCurrency : balance.currency}:${!form.amount ? "0" : form.amount
}`,
);
@@ -268,32 +265,32 @@ function CreateCashout({
calculateFromDebit(amount, sellFee, sellRate)
.then((r) => {
setCalc(r);
- saveError(undefined);
})
.catch((error) => {
- saveError(
+ notify(
error instanceof RequestError
? buildRequestErrorMessage(i18n, error.cause)
: {
- title: i18n.str`Could not estimate the cashout`,
- description: error.message,
- },
+ type: "error",
+ title: i18n.str`Could not estimate the cashout`,
+ description: error.message as TranslatedString
+ },
);
});
} else {
calculateFromCredit(amount, sellFee, sellRate)
.then((r) => {
setCalc(r);
- saveError(undefined);
})
.catch((error) => {
- saveError(
+ notify(
error instanceof RequestError
? buildRequestErrorMessage(i18n, error.cause)
: {
- title: i18n.str`Could not estimate the cashout`,
- description: error.message,
- },
+ type: "error",
+ title: i18n.str`Could not estimate the cashout`,
+ description: error.message,
+ },
);
});
}
@@ -308,22 +305,19 @@ function CreateCashout({
amount: !form.amount
? i18n.str`required`
: !amount
- ? i18n.str`could not be parsed`
- : Amounts.cmp(limit, calc.debit) === -1
- ? i18n.str`balance is not enough`
- : Amounts.cmp(calc.beforeFee, sellFee) === -1
- ? i18n.str`the total amount to transfer does not cover the fees`
- : Amounts.isZero(calc.credit)
- ? i18n.str`the total transfer at destination will be zero`
- : undefined,
+ ? i18n.str`could not be parsed`
+ : Amounts.cmp(limit, calc.debit) === -1
+ ? i18n.str`balance is not enough`
+ : Amounts.cmp(calc.beforeFee, sellFee) === -1
+ ? i18n.str`the total amount to transfer does not cover the fees`
+ : Amounts.isZero(calc.credit)
+ ? i18n.str`the total transfer at destination will be zero`
+ : undefined,
channel: !form.channel ? i18n.str`required` : undefined,
});
return (
<div>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
<h1>New cashout</h1>
<form class="pure-form">
<fieldset>
@@ -341,13 +335,15 @@ function CreateCashout({
/>
</fieldset>
<fieldset>
- <label>
+ <label for="amount">
{form.isDebit
? i18n.str`Amount to send`
: i18n.str`Amount to receive`}
+
</label>
<div style={{ display: "flex" }}>
- <Amount
+ <InputAmount
+ name="amount"
currency={amount.currency}
value={form.amount}
onChange={(v) => {
@@ -362,7 +358,6 @@ function CreateCashout({
type="checkbox"
name="asd"
onChange={(e): void => {
- console.log("asdasd", form.isDebit);
form.isDebit = !form.isDebit;
updateForm(structuredClone(form));
}}
@@ -376,24 +371,27 @@ function CreateCashout({
<input value={sellRate} disabled />
</fieldset>
<fieldset>
- <label>{i18n.str`Balance now`}</label>
- <Amount
+ <label for="balance-now">{i18n.str`Balance now`}</label>
+ <InputAmount
+ name="banace-now"
currency={balance.currency}
value={Amounts.stringifyValue(balance)}
/>
</fieldset>
<fieldset>
- <label
+ <label for="total-cost"
style={{ fontWeight: "bold", color: "red" }}
>{i18n.str`Total cost`}</label>
- <Amount
+ <InputAmount
+ name="total-cost"
currency={balance.currency}
value={Amounts.stringifyValue(calc.debit)}
/>
</fieldset>
<fieldset>
- <label>{i18n.str`Balance after`}</label>
- <Amount
+ <label for="balance-after">{i18n.str`Balance after`}</label>
+ <InputAmount
+ name="balance-after"
currency={balance.currency}
value={balanceAfter ? Amounts.stringifyValue(balanceAfter) : ""}
/>
@@ -401,16 +399,18 @@ function CreateCashout({
{Amounts.isZero(sellFee) ? undefined : (
<Fragment>
<fieldset>
- <label>{i18n.str`Amount after conversion`}</label>
- <Amount
+ <label for="amount-conversiojn">{i18n.str`Amount after conversion`}</label>
+ <InputAmount
+ name="amount-conversion"
currency={fiatCurrency}
value={Amounts.stringifyValue(calc.beforeFee)}
/>
</fieldset>
<fieldset>
- <label>{i18n.str`Cashout fee`}</label>
- <Amount
+ <label form="cashout-fee">{i18n.str`Cashout fee`}</label>
+ <InputAmount
+ name="cashout-fee"
currency={fiatCurrency}
value={Amounts.stringifyValue(sellFee)}
/>
@@ -418,10 +418,11 @@ function CreateCashout({
</Fragment>
)}
<fieldset>
- <label
+ <label for="total"
style={{ fontWeight: "bold", color: "green" }}
>{i18n.str`Total cashout transfer`}</label>
- <Amount
+ <InputAmount
+ name="total"
currency={fiatCurrency}
value={Amounts.stringifyValue(calc.credit)}
/>
@@ -511,18 +512,18 @@ function CreateCashout({
onComplete(res.data.uuid);
} catch (error) {
if (error instanceof RequestError) {
- saveError(
+ notify(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.BadRequest
? i18n.str`The exchange rate was incorrectly applied`
: status === HttpStatusCode.Forbidden
- ? i18n.str`A institutional user tried the operation`
- : status === HttpStatusCode.Conflict
- ? i18n.str`Need a contact data where to send the TAN`
- : status === HttpStatusCode.PreconditionFailed
- ? i18n.str`The account does not have sufficient funds`
- : undefined,
+ ? i18n.str`A institutional user tried the operation`
+ : status === HttpStatusCode.Conflict
+ ? i18n.str`Need a contact data where to send the TAN`
+ : status === HttpStatusCode.PreconditionFailed
+ ? i18n.str`The account does not have sufficient funds`
+ : undefined,
onServerError: (status) =>
status === HttpStatusCode.ServiceUnavailable
? i18n.str`The bank does not support the TAN channel for this operation`
@@ -530,13 +531,12 @@ function CreateCashout({
}),
);
} else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
}
}
}}
@@ -565,7 +565,6 @@ export function ShowCashoutDetails({
const result = useCashoutDetails(id);
const { abortCashout, confirmCashout } = useCircuitAccountAPI();
const [code, setCode] = useState<string | undefined>(undefined);
- const [error, saveError] = useState<ErrorMessage | undefined>();
if (!result.ok) return onLoadNotOk(result);
const errors = undefinedIfEmpty({
code: !code ? i18n.str`required` : undefined,
@@ -574,9 +573,6 @@ export function ShowCashoutDetails({
return (
<div>
<h1>Cashout details {id}</h1>
- {error && (
- <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
- )}
<form class="pure-form">
<fieldset>
<label>
@@ -661,24 +657,23 @@ export function ShowCashoutDetails({
onCancel();
} catch (error) {
if (error instanceof RequestError) {
- saveError(
+ notify(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.NotFound
? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
: status === HttpStatusCode.PreconditionFailed
- ? i18n.str`Cashout was already confimed`
- : undefined,
+ ? i18n.str`Cashout was already confimed`
+ : undefined,
}),
);
} else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
}
}
}}
@@ -699,28 +694,27 @@ export function ShowCashoutDetails({
});
} catch (error) {
if (error instanceof RequestError) {
- saveError(
+ notify(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.NotFound
? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
: status === HttpStatusCode.PreconditionFailed
- ? i18n.str`Cashout was already confimed`
- : status === HttpStatusCode.Conflict
- ? i18n.str`Confirmation failed. Maybe the user changed their cash-out address between the creation and the confirmation`
- : status === HttpStatusCode.Forbidden
- ? i18n.str`Invalid code`
- : undefined,
+ ? i18n.str`Cashout was already confimed`
+ : status === HttpStatusCode.Conflict
+ ? i18n.str`Confirmation failed. Maybe the user changed their cash-out address between the creation and the confirmation`
+ : status === HttpStatusCode.Forbidden
+ ? i18n.str`Invalid code`
+ : undefined,
}),
);
} else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
}
}
}}
diff --git a/packages/demobank-ui/src/pages/rnd.ts b/packages/demobank-ui/src/pages/rnd.ts
new file mode 100644
index 000000000..32c3a934f
--- /dev/null
+++ b/packages/demobank-ui/src/pages/rnd.ts
@@ -0,0 +1,2895 @@
+import { createEddsaKeyPair, encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"
+import { bankUiSettings } from "../settings.js"
+
+
+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",
+ "wee",
+ "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 {
+ if (bankUiSettings.simplePasswordForRandomAccounts) return "123"
+ return encodeCrock(getRandomBytes(16))
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/scss/DurationPicker.scss b/packages/demobank-ui/src/scss/DurationPicker.scss
deleted file mode 100644
index aa75b9916..000000000
--- a/packages/demobank-ui/src/scss/DurationPicker.scss
+++ /dev/null
@@ -1,70 +0,0 @@
-.rdp-picker {
- display: flex;
- height: 175px;
-}
-
-@media (max-width: 400px) {
- .rdp-picker {
- width: 250px;
- }
-}
-
-.rdp-masked-div {
- overflow: hidden;
- height: 175px;
- position: relative;
-}
-
-.rdp-column-container {
- flex-grow: 1;
- display: inline-block;
-}
-
-.rdp-column {
- position: absolute;
- z-index: 0;
- width: 100%;
-}
-
-.rdp-reticule {
- border: 0;
- border-top: 2px solid rgba(109, 202, 236, 1);
- height: 2px;
- position: absolute;
- width: 80%;
- margin: 0;
- z-index: 100;
- left: 50%;
- -webkit-transform: translateX(-50%);
- transform: translateX(-50%);
-}
-
-.rdp-text-overlay {
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 35px;
- font-size: 20px;
- left: 50%;
- -webkit-transform: translateX(-50%);
- transform: translateX(-50%);
-}
-
-.rdp-cell div {
- font-size: 17px;
- color: gray;
- font-style: italic;
-}
-
-.rdp-cell {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 35px;
- font-size: 18px;
-}
-
-.rdp-center {
- font-size: 25px;
-}
diff --git a/packages/demobank-ui/src/scss/_aside.scss b/packages/demobank-ui/src/scss/_aside.scss
deleted file mode 100644
index 11809990b..000000000
--- a/packages/demobank-ui/src/scss/_aside.scss
+++ /dev/null
@@ -1,128 +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)
- */
-
-html {
- &.has-aside-left {
- &.has-aside-expanded {
- nav.navbar,
- body {
- padding-left: $aside-width;
- }
- }
- aside.is-placed-left {
- display: block;
- }
- }
-}
-
-aside.aside.is-expanded {
- width: $aside-width;
-
- .menu-list {
- @include icon-with-update-mark($aside-icon-width);
-
- span.menu-item-label {
- display: inline-block;
- }
-
- li.is-active {
- ul {
- display: block;
- }
- background-color: $body-background-color;
- }
- }
-}
-
-aside.aside {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- z-index: 40;
- height: 100vh;
- padding: 0;
- box-shadow: $aside-box-shadow;
- background: $aside-background-color;
-
- .aside-tools {
- display: flex;
- flex-direction: row;
- width: 100%;
- background-color: $aside-tools-background-color;
- color: $aside-tools-color;
- line-height: $navbar-height;
- height: $navbar-height;
- padding-left: $default-padding * 0.5;
- flex: 1;
-
- .icon {
- margin-right: $default-padding * 0.5;
- }
- }
-
- .menu-list {
- li {
- a {
- &.has-dropdown-icon {
- position: relative;
- padding-right: $aside-icon-width;
-
- .dropdown-icon {
- position: absolute;
- top: $size-base * 0.5;
- right: 0;
- }
- }
- }
- ul {
- display: none;
- border-left: 0;
- background-color: darken($base-color, 2.5%);
- padding-left: 0;
- margin: 0 0 $default-padding * 0.5;
-
- li {
- a {
- padding: $default-padding * 0.5 0 $default-padding * 0.5
- $default-padding * 0.5;
- font-size: $aside-submenu-font-size;
-
- &.has-icon {
- padding-left: 0;
- }
- &.is-active {
- &:not(:hover) {
- background: transparent;
- }
- }
- }
- }
- }
- }
- }
-
- .menu-label {
- padding: 0 $default-padding * 0.5;
- margin-top: $default-padding * 0.5;
- margin-bottom: $default-padding * 0.5;
- }
-}
diff --git a/packages/demobank-ui/src/scss/_card.scss b/packages/demobank-ui/src/scss/_card.scss
deleted file mode 100644
index 3f71aeb6a..000000000
--- a/packages/demobank-ui/src/scss/_card.scss
+++ /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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-.card:not(:last-child) {
- margin-bottom: $default-padding;
-}
-
-.card {
- border-radius: $radius-large;
- border: $card-border;
-
- &.has-table {
- .card-content {
- padding: 0;
- }
- .b-table {
- border-radius: $radius-large;
- overflow: hidden;
- }
- }
-
- &.is-card-widget {
- .card-content {
- padding: $default-padding * 0.5;
- }
- }
-
- .card-header {
- border-bottom: 1px solid $base-color-light;
- }
-
- .card-content {
- hr {
- margin-left: $card-content-padding * -1;
- margin-right: $card-content-padding * -1;
- }
- }
-
- .is-widget-icon {
- .icon {
- width: 5rem;
- height: 5rem;
- }
- }
-
- .is-widget-label {
- .subtitle {
- color: $grey;
- }
- }
-}
diff --git a/packages/demobank-ui/src/scss/_custom-calendar.scss b/packages/demobank-ui/src/scss/_custom-calendar.scss
deleted file mode 100644
index 463cd88d3..000000000
--- a/packages/demobank-ui/src/scss/_custom-calendar.scss
+++ /dev/null
@@ -1,263 +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/>
- */
-
-:root {
- --primary-color: #3298dc;
-
- --primary-text-color-dark: rgba(0, 0, 0, 0.87);
- --secondary-text-color-dark: rgba(0, 0, 0, 0.57);
- --disabled-text-color-dark: rgba(0, 0, 0, 0.13);
-
- --primary-text-color-light: rgba(255, 255, 255, 0.87);
- --secondary-text-color-light: rgba(255, 255, 255, 0.57);
- --disabled-text-color-light: rgba(255, 255, 255, 0.13);
-
- --font-stack: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
-
- --primary-card-color: #fff;
- --primary-background-color: #f2f2f2;
-
- --box-shadow-lvl-1: 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 1px 2px rgba(0, 0, 0, 0.24);
- --box-shadow-lvl-2: 0 3px 6px rgba(0, 0, 0, 0.16),
- 0 3px 6px rgba(0, 0, 0, 0.23);
- --box-shadow-lvl-3: 0 10px 20px rgba(0, 0, 0, 0.19),
- 0 6px 6px rgba(0, 0, 0, 0.23);
- --box-shadow-lvl-4: 0 14px 28px rgba(0, 0, 0, 0.25),
- 0 10px 10px rgba(0, 0, 0, 0.22);
-}
-
-.home .datePicker div {
- margin-top: 0px;
- margin-bottom: 0px;
-}
-.datePicker {
- text-align: left;
- background: var(--primary-card-color);
- border-radius: 3px;
- z-index: 200;
- position: fixed;
- height: auto;
- max-height: 90vh;
- width: 90vw;
- max-width: 448px;
- transform-origin: top left;
- transition: transform 0.22s ease-in-out, opacity 0.22s ease-in-out;
- top: 50%;
- left: 50%;
- opacity: 0;
- transform: scale(0) translate(-50%, -50%);
- user-select: none;
-
- &.datePicker--opened {
- opacity: 1;
- transform: scale(1) translate(-50%, -50%);
- }
-
- .datePicker--titles {
- border-top-left-radius: 3px;
- border-top-right-radius: 3px;
- padding: 24px;
- height: 100px;
- background: var(--primary-color);
-
- h2,
- h3 {
- cursor: pointer;
- color: #fff;
- line-height: 1;
- padding: 0;
- margin: 0;
- font-size: 32px;
- }
-
- h3 {
- color: rgba(255, 255, 255, 0.57);
- font-size: 18px;
- padding-bottom: 2px;
- }
- }
-
- nav {
- padding: 20px;
- height: 56px;
-
- h4 {
- width: calc(100% - 60px);
- text-align: center;
- display: inline-block;
- padding: 0;
- font-size: 14px;
- line-height: 24px;
- margin: 0;
- position: relative;
- top: -9px;
- color: var(--primary-text-color);
- }
-
- i {
- cursor: pointer;
- color: var(--secondary-text-color);
- font-size: 26px;
- user-select: none;
- border-radius: 50%;
-
- &:hover {
- background: var(--disabled-text-color-dark);
- }
- }
- }
-
- .datePicker--scroll {
- overflow-y: auto;
- max-height: calc(90vh - 56px - 100px);
- }
-
- .datePicker--calendar {
- padding: 0 20px;
-
- .datePicker--dayNames {
- width: 100%;
- display: grid;
- text-align: center;
-
- // there's probably a better way to do this, but wanted to try out CSS grid
- grid-template-columns:
- calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7)
- calc(100% / 7) calc(100% / 7) calc(100% / 7);
-
- span {
- color: var(--secondary-text-color-dark);
- font-size: 14px;
- line-height: 42px;
- display: inline-grid;
- }
- }
-
- .datePicker--days {
- width: 100%;
- display: grid;
- text-align: center;
- grid-template-columns:
- calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7)
- calc(100% / 7) calc(100% / 7) calc(100% / 7);
-
- span {
- color: var(--primary-text-color-dark);
- line-height: 42px;
- font-size: 14px;
- display: inline-grid;
- transition: color 0.22s;
- height: 42px;
- position: relative;
- cursor: pointer;
- user-select: none;
- border-radius: 50%;
-
- &::before {
- content: "";
- position: absolute;
- z-index: -1;
- height: 42px;
- width: 42px;
- left: calc(50% - 21px);
- background: var(--primary-color);
- border-radius: 50%;
- transition: transform 0.22s, opacity 0.22s;
- transform: scale(0);
- opacity: 0;
- }
-
- &[disabled="true"] {
- cursor: unset;
- }
-
- &.datePicker--today {
- font-weight: 700;
- }
-
- &.datePicker--selected {
- color: rgba(255, 255, 255, 0.87);
-
- &:before {
- transform: scale(1);
- opacity: 1;
- }
- }
- }
- }
- }
-
- .datePicker--selectYear {
- padding: 0 20px;
- display: block;
- width: 100%;
- text-align: center;
- max-height: 362px;
-
- span {
- display: block;
- width: 100%;
- font-size: 24px;
- margin: 20px auto;
- cursor: pointer;
-
- &.selected {
- font-size: 42px;
- color: var(--primary-color);
- }
- }
- }
-
- div.datePicker--actions {
- width: 100%;
- padding: 8px;
- text-align: right;
-
- button {
- margin-bottom: 0;
- font-size: 15px;
- cursor: pointer;
- color: var(--primary-text-color);
- border: none;
- margin-left: 8px;
- min-width: 64px;
- line-height: 36px;
- background-color: transparent;
- appearance: none;
- padding: 0 16px;
- border-radius: 3px;
- transition: background-color 0.13s;
-
- &:hover,
- &:focus {
- outline: none;
- background-color: var(--disabled-text-color-dark);
- }
- }
- }
-}
-
-.datePicker--background {
- z-index: 199;
- position: fixed;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- background: rgba(0, 0, 0, 0.52);
- animation: fadeIn 0.22s forwards;
-}
diff --git a/packages/demobank-ui/src/scss/_form.scss b/packages/demobank-ui/src/scss/_form.scss
deleted file mode 100644
index 9d93477fd..000000000
--- a/packages/demobank-ui/src/scss/_form.scss
+++ /dev/null
@@ -1,71 +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)
- */
-
-.field {
- &.has-check {
- .field-body {
- margin-top: $default-padding * 0.125;
- }
- }
- .control {
- .mdi-24px.mdi-set,
- .mdi-24px.mdi:before {
- font-size: inherit;
- }
- }
-}
-.upload {
- .upload-draggable {
- display: block;
- }
-}
-
-.input,
-.textarea,
-select {
- box-shadow: none;
-
- &:focus,
- &:active {
- box-shadow: none !important;
- }
-}
-
-.switch input[type="checkbox"] + .check:before {
- box-shadow: none;
-}
-
-.switch,
-.b-checkbox.checkbox {
- input[type="checkbox"] {
- &:focus + .check,
- &:focus:checked + .check {
- box-shadow: none !important;
- }
- }
-}
-
-.b-checkbox.checkbox input[type="checkbox"],
-.b-radio.radio input[type="radio"] {
- & + .check {
- border: $checkbox-border;
- }
-}
diff --git a/packages/demobank-ui/src/scss/_hero-bar.scss b/packages/demobank-ui/src/scss/_hero-bar.scss
deleted file mode 100644
index 31b7e623e..000000000
--- a/packages/demobank-ui/src/scss/_hero-bar.scss
+++ /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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-section.hero.is-hero-bar {
- background-color: $hero-bar-background;
- border-bottom: $light-border;
-
- .hero-body {
- padding: $default-padding;
-
- .level-item {
- &.is-hero-avatar-item {
- margin-right: $default-padding;
- }
-
- > div > .level {
- margin-bottom: $default-padding * 0.5;
- }
-
- .subtitle + p {
- margin-top: $default-padding * 0.5;
- }
- }
-
- .button {
- &.is-hero-button {
- background-color: rgba($white, 0.5);
- font-weight: 300;
- @include transition(background-color);
-
- &:hover {
- background-color: $white;
- }
- }
- }
- }
-}
diff --git a/packages/demobank-ui/src/scss/_loading.scss b/packages/demobank-ui/src/scss/_loading.scss
deleted file mode 100644
index d25bf8048..000000000
--- a/packages/demobank-ui/src/scss/_loading.scss
+++ /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/>
- */
-
-.lds-ring {
- display: inline-block;
- position: relative;
- width: 80px;
- height: 80px;
-}
-.lds-ring div {
- box-sizing: border-box;
- display: block;
- position: absolute;
- width: 64px;
- height: 64px;
- margin: 8px;
- border: 8px solid black;
- border-radius: 50%;
- animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
- border-color: black transparent transparent transparent;
-}
-.lds-ring div:nth-child(1) {
- animation-delay: -0.45s;
-}
-.lds-ring div:nth-child(2) {
- animation-delay: -0.3s;
-}
-.lds-ring div:nth-child(3) {
- animation-delay: -0.15s;
-}
-@keyframes lds-ring {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
-}
diff --git a/packages/demobank-ui/src/scss/_main-section.scss b/packages/demobank-ui/src/scss/_main-section.scss
deleted file mode 100644
index 01edc24bf..000000000
--- a/packages/demobank-ui/src/scss/_main-section.scss
+++ /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)
- */
-
-section.section.is-main-section {
- padding-top: $default-padding;
-}
diff --git a/packages/demobank-ui/src/scss/_nav-bar.scss b/packages/demobank-ui/src/scss/_nav-bar.scss
deleted file mode 100644
index c6dd04263..000000000
--- a/packages/demobank-ui/src/scss/_nav-bar.scss
+++ /dev/null
@@ -1,144 +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)
- */
-
-nav.navbar {
- box-shadow: $navbar-box-shadow;
-
- .navbar-item {
- &.has-user-avatar {
- .is-user-avatar {
- margin-right: $default-padding * 0.5;
- display: inline-flex;
- width: $navbar-avatar-size;
- height: $navbar-avatar-size;
- }
- }
-
- &.has-divider {
- border-right: $navbar-divider-border;
- }
-
- &.no-left-space {
- padding-left: 0;
- }
-
- &.has-dropdown {
- padding-right: 0;
- padding-left: 0;
-
- .navbar-link {
- padding-right: $navbar-item-h-padding;
- padding-left: $navbar-item-h-padding;
- }
- }
-
- &.has-control {
- padding-top: 0;
- padding-bottom: 0;
- }
-
- .control {
- .input {
- color: $navbar-input-color;
- border: 0;
- box-shadow: none;
- background: transparent;
-
- &::placeholder {
- color: $navbar-input-placeholder-color;
- }
- }
- }
- }
-}
-
-@include touch {
- nav.navbar {
- display: flex;
- padding-right: 0;
-
- .navbar-brand {
- flex: 1;
-
- &.is-right {
- flex: none;
- }
- }
-
- .navbar-item {
- &.no-left-space-touch {
- padding-left: 0;
- }
- }
-
- .navbar-menu {
- position: absolute;
- width: 100vw;
- padding-top: 0;
- top: $navbar-height;
- left: 0;
-
- .navbar-item {
- .icon:first-child {
- margin-right: $default-padding * 0.5;
- }
-
- &.has-dropdown {
- > .navbar-link {
- background-color: $white-ter;
- .icon:last-child {
- display: none;
- }
- }
- }
-
- &.has-user-avatar {
- > .navbar-link {
- display: flex;
- align-items: center;
- padding-top: $default-padding * 0.5;
- padding-bottom: $default-padding * 0.5;
- }
- }
- }
- }
- }
-}
-
-@include desktop {
- nav.navbar {
- .navbar-item {
- padding-right: $navbar-item-h-padding;
- padding-left: $navbar-item-h-padding;
-
- &:not(.is-desktop-icon-only) {
- .icon:first-child {
- margin-right: $default-padding * 0.5;
- }
- }
- &.is-desktop-icon-only {
- span:not(.icon) {
- display: none;
- }
- }
- }
- }
-}
diff --git a/packages/demobank-ui/src/scss/_table.scss b/packages/demobank-ui/src/scss/_table.scss
deleted file mode 100644
index b68d50e4f..000000000
--- a/packages/demobank-ui/src/scss/_table.scss
+++ /dev/null
@@ -1,179 +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)
- */
-
-table.table {
- thead {
- th {
- border-bottom-width: 1px;
- }
- }
-
- td,
- th {
- &.checkbox-cell {
- .b-checkbox.checkbox:not(.button) {
- margin-right: 0;
- width: 20px;
-
- .control-label {
- display: none;
- padding: 0;
- }
- }
- }
- }
-
- td {
- .image {
- margin: 0 auto;
- width: $table-avatar-size;
- height: $table-avatar-size;
- }
-
- &.is-progress-col {
- min-width: 5rem;
- vertical-align: middle;
- }
- }
-}
-
-.b-table {
- .table {
- border: 0;
- border-radius: 0;
- }
-
- /* This stylizes buefy's pagination */
- .table-wrapper {
- margin-bottom: 0;
- }
-
- .table-wrapper + .level {
- padding: $notification-padding;
- padding-left: $card-content-padding;
- padding-right: $card-content-padding;
- margin: 0;
- border-top: $base-color-light;
- background: $notification-background-color;
-
- .pagination-link {
- background: $button-background-color;
- color: $button-color;
- border-color: $button-border-color;
-
- &.is-current {
- border-color: $button-active-border-color;
- }
- }
-
- .pagination-previous,
- .pagination-next,
- .pagination-link {
- border-color: $button-border-color;
- color: $base-color;
-
- &[disabled] {
- background-color: transparent;
- }
- }
- }
-}
-
-@include mobile {
- .card {
- &.has-table {
- .b-table {
- .table-wrapper + .level {
- .level-left + .level-right {
- margin-top: 0;
- }
- }
- }
- }
- &.has-mobile-sort-spaced {
- .b-table {
- .field.table-mobile-sort {
- padding-top: $default-padding * 0.5;
- }
- }
- }
- }
- .b-table {
- .field.table-mobile-sort {
- padding: 0 $default-padding * 0.5;
- }
-
- .table-wrapper.has-mobile-cards {
- tr {
- box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
- margin-bottom: 3px !important;
- }
- td {
- &.is-progress-col {
- span,
- progress {
- display: flex;
- width: 45%;
- align-items: center;
- align-self: center;
- }
- }
-
- &.checkbox-cell,
- &.is-image-cell {
- border-bottom: 0 !important;
- }
-
- &.checkbox-cell,
- &.is-actions-cell {
- &:before {
- display: none;
- }
- }
-
- &.has-no-head-mobile {
- &:before {
- display: none;
- }
-
- span {
- display: block;
- width: 100%;
- }
-
- &.is-progress-col {
- progress {
- width: 100%;
- }
- }
-
- &.is-image-cell {
- .image {
- width: $table-avatar-size-mobile;
- height: auto;
- margin: 0 auto $default-padding * 0.25;
- }
- }
- }
- }
- }
- }
-}
diff --git a/packages/demobank-ui/src/scss/_theme-default.scss b/packages/demobank-ui/src/scss/_theme-default.scss
deleted file mode 100644
index 538dfd4da..000000000
--- a/packages/demobank-ui/src/scss/_theme-default.scss
+++ /dev/null
@@ -1,136 +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)
- */
-
-/* We'll need some initial vars to use here */
-@import "node_modules/bulma/sass/utilities/initial-variables";
-
-/* Base: Size */
-$size-base: 1rem;
-$default-padding: $size-base * 1.5;
-
-/* Default font */
-$family-sans-serif: "Nunito", sans-serif;
-
-/* Base color */
-$base-color: #2e323a;
-$base-color-light: rgba(24, 28, 33, 0.06);
-
-/* General overrides */
-$primary: $turquoise;
-$body-background-color: #f8f8f8;
-$link: $blue;
-$link-visited: $purple;
-$light-border: 1px solid $base-color-light;
-$hr-height: 1px;
-
-/* NavBar: specifics */
-$navbar-input-color: $grey-darker;
-$navbar-input-placeholder-color: $grey-lighter;
-$navbar-box-shadow: 0 1px 0 rgba(24, 28, 33, 0.04);
-$navbar-divider-border: 1px solid rgba($grey-lighter, 0.25);
-$navbar-item-h-padding: $default-padding * 0.75;
-$navbar-avatar-size: 1.75rem;
-
-/* Aside: Bulma override */
-$menu-item-radius: 0;
-$menu-list-link-padding: $size-base * 0.5 0;
-$menu-label-color: lighten($base-color, 25%);
-$menu-item-color: lighten($base-color, 30%);
-$menu-item-hover-color: $white;
-$menu-item-hover-background-color: darken($base-color, 3.5%);
-$menu-item-active-color: $white;
-$menu-item-active-background-color: darken($base-color, 2.5%);
-
-/* Aside: specifics */
-$aside-width: $size-base * 14;
-$aside-mobile-width: $size-base * 15;
-$aside-icon-width: $size-base * 3;
-$aside-submenu-font-size: $size-base * 0.95;
-$aside-box-shadow: none;
-$aside-background-color: $base-color;
-$aside-tools-background-color: darken($aside-background-color, 10%);
-$aside-tools-color: $white;
-
-/* Title Bar: specifics */
-$title-bar-color: $grey;
-$title-bar-active-color: $black-ter;
-
-/* Hero Bar: specifics */
-$hero-bar-background: $white;
-
-/* Card: Bulma override */
-$card-shadow: none;
-$card-header-shadow: none;
-
-/* Card: specifics */
-$card-border: 1px solid $base-color-light;
-$card-header-border-bottom-color: $base-color-light;
-
-/* Table: Bulma override */
-$table-cell-border: 1px solid $white-bis;
-
-/* Table: specifics */
-$table-avatar-size: $size-base * 1.5;
-$table-avatar-size-mobile: 25vw;
-
-/* Form */
-$checkbox-border: 1px solid $base-color;
-
-/* Modal card: Bulma override */
-$modal-card-head-background-color: $white-ter;
-$modal-card-title-size: $size-base;
-$modal-card-body-padding: $default-padding 20px;
-$modal-card-head-border-bottom: 1px solid $white-ter;
-$modal-card-foot-border-top: 0;
-
-/* Modal card: specifics */
-$modal-card-width: 80vw;
-$modal-card-width-mobile: 90vw;
-$modal-card-foot-background-color: $white-ter;
-
-/* Notification: Bulma override */
-$notification-padding: $default-padding * 0.75 $default-padding;
-
-/* Footer: Bulma override */
-$footer-background-color: $white;
-$footer-padding: $default-padding * 0.33 $default-padding;
-
-/* Footer: specifics */
-$footer-logo-height: $size-base * 2;
-
-/* Progress: Bulma override */
-$progress-bar-background-color: $grey-lighter;
-
-/* Icon: specifics */
-$icon-update-mark-size: $size-base * 0.5;
-$icon-update-mark-color: $yellow;
-
-$input-disabled-border-color: $grey-lighter;
-$table-row-hover-background-color: hsl(0, 0%, 80%);
-
-.menu-list {
- div {
- border-radius: $menu-item-radius;
- color: $menu-item-color;
- display: block;
- padding: $menu-list-link-padding;
- }
-}
diff --git a/packages/demobank-ui/src/scss/bank.scss b/packages/demobank-ui/src/scss/bank.scss
deleted file mode 100644
index f8de0a984..000000000
--- a/packages/demobank-ui/src/scss/bank.scss
+++ /dev/null
@@ -1,353 +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;
-}
-
-.top-right {
- float: right;
-}
-.some-space {
- display: inline-block;
- 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: min-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: min-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: min-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: min-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;
-}
-
-.pure-form > fieldset > label {
- display: block;
-}
-.pure-form > fieldset > input[disabled] {
- color: black !important;
-}
-.pure-form > fieldset > div > input[disabled] {
- color: black !important;
-}
-
-.pure-form > fieldset > div.channel > div {
- display: inline-block;
- margin: 1em;
- border: 1px black solid;
- width: fit-content;
- padding: 0.4em;
- cursor: pointer;
-}
-
-.button-success {
- background: rgb(28, 184, 65);
- /* this is a green */
-}
-
-.button-error {
- background: rgb(202, 60, 60);
- /* this is a maroon */
-}
-
-.button-warning {
- background: rgb(223, 117, 20);
- /* this is an orange */
-}
-
-.button-secondary {
- background: rgb(66, 184, 221);
- /* this is a light blue */
-}
-
-[name=wire-transfer-form] > input {
- margin-bottom: 1em;
-
-}
-
-.lds-ring {
- display: inline-block;
- position: relative;
- width: 80px;
- height: 80px;
-}
-.lds-ring div {
- box-sizing: border-box;
- display: block;
- position: absolute;
- width: 64px;
- height: 64px;
- margin: 8px;
- border: 8px solid black;
- border-radius: 50%;
- animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
- border-color: black transparent transparent transparent;
-}
-.lds-ring div:nth-child(1) {
- animation-delay: -0.45s;
-}
-.lds-ring div:nth-child(2) {
- animation-delay: -0.3s;
-}
-.lds-ring div:nth-child(3) {
- animation-delay: -0.15s;
-}
-@keyframes lds-ring {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
-}
diff --git a/packages/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 c2d9fa903..000000000
--- a/packages/demobank-ui/src/scss/demo.scss
+++ /dev/null
@@ -1,167 +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: 1em;
- margin-right: 1em;
- 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: 100;
-}
-
-nav {
- // left: 1vw;
- position: relative;
- background: #0042b2;
- z-index: 100;
-}
-
-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: 8px;
-}
-
-
-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: 100px;
- text-align: left;
-}
-
-.skip {
- position: absolute;
- left: -10000px;
- top: auto;
- width: 1px;
- height: 1px;
- overflow: hidden;
-}
-
-.skip:focus {
- position: static;
- width: auto;
- height: auto;
-}
-
-.demolist > a {
- margin: 8px;
-}
-
-.buttons-account input.pure-button {
- margin: 8px;
-} \ No newline at end of file
diff --git a/packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf b/packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf
deleted file mode 100644
index 7665ee336..000000000
--- a/packages/demobank-ui/src/scss/fonts/XRXV3I6Li01BKofINeaE.ttf
+++ /dev/null
Binary files differ
diff --git a/packages/demobank-ui/src/scss/fonts/nunito.css b/packages/demobank-ui/src/scss/fonts/nunito.css
deleted file mode 100644
index 8d45df9a1..000000000
--- a/packages/demobank-ui/src/scss/fonts/nunito.css
+++ /dev/null
@@ -1,22 +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/>
- */
-
-@font-face {
- font-family: "Nunito";
- font-style: normal;
- font-weight: 400;
- src: url(./XRXV3I6Li01BKofINeaE.ttf) format("truetype");
-}
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot b/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
deleted file mode 100644
index ab6b25ded..000000000
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.eot
+++ /dev/null
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf b/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
deleted file mode 100644
index 824be10fa..000000000
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.ttf
+++ /dev/null
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff b/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
deleted file mode 100644
index 7e087c1de..000000000
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff
+++ /dev/null
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2 b/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
deleted file mode 100644
index b5caa4ddc..000000000
--- a/packages/demobank-ui/src/scss/icons/fonts/materialdesignicons-webfont-4.9.95.woff2
+++ /dev/null
Binary files differ
diff --git a/packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css b/packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
deleted file mode 100644
index 2b8a2b244..000000000
--- a/packages/demobank-ui/src/scss/icons/materialdesignicons-4.9.95.min.css
+++ /dev/null
@@ -1,15109 +0,0 @@
-@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/demobank-ui/src/scss/libs/_all.scss b/packages/demobank-ui/src/scss/libs/_all.scss
deleted file mode 100644
index d33f8acc4..000000000
--- a/packages/demobank-ui/src/scss/libs/_all.scss
+++ /dev/null
@@ -1,29 +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 "node_modules/bulma-radio/bulma-radio";
-// @import "node_modules/bulma-responsive-tables/bulma-responsive-tables";
-@import "node_modules/bulma-checkbox/bulma-checkbox";
-// @import "node_modules/bulma-switch-control/bulma-switch-control";
-// @import "node_modules/bulma-upload-control/bulma-upload-control";
-
-/* Bulma */
-@import "node_modules/bulma/bulma";
diff --git a/packages/demobank-ui/src/scss/main.css b/packages/demobank-ui/src/scss/main.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/packages/demobank-ui/src/scss/main.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/demobank-ui/src/scss/main.scss b/packages/demobank-ui/src/scss/main.scss
deleted file mode 100644
index b9a46718f..000000000
--- a/packages/demobank-ui/src/scss/main.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-@use "pure";
-@use "bank";
-@use "demo";
-@use "toggle";
-@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 25a261a5f..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/scss/toggle.scss b/packages/demobank-ui/src/scss/toggle.scss
deleted file mode 100644
index 24636da2f..000000000
--- a/packages/demobank-ui/src/scss/toggle.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-$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/demobank-ui/src/settings.ts b/packages/demobank-ui/src/settings.ts
index 6c78d287b..44a016de6 100644
--- a/packages/demobank-ui/src/settings.ts
+++ b/packages/demobank-ui/src/settings.ts
@@ -15,11 +15,14 @@
*/
export interface BankUiSettings {
- backendBaseURL: string;
- allowRegistrations: boolean;
- showDemoNav: boolean;
- bankName: string;
- demoSites: [string, string][];
+ backendBaseURL?: string;
+ allowRegistrations?: boolean;
+ iconLinkURL?: string;
+ showDemoNav?: boolean;
+ simplePasswordForRandomAccounts?: boolean;
+ allowRandomAccountCreation?: boolean;
+ bankName?: string;
+ demoSites?: [string, string][];
}
/**
@@ -27,9 +30,12 @@ export interface BankUiSettings {
*/
const defaultSettings: BankUiSettings = {
backendBaseURL: "https://bank.demo.taler.net/demobanks/default/",
+ iconLinkURL: "https://demo.taler.net/",
allowRegistrations: true,
bankName: "Taler Bank",
showDemoNav: true,
+ simplePasswordForRandomAccounts: true,
+ allowRandomAccountCreation: true,
demoSites: [
["Landing", "https://demo.taler.net/"],
["Bank", "https://bank.demo.taler.net/"],
diff --git a/packages/demobank-ui/src/stories.test.ts b/packages/demobank-ui/src/stories.test.ts
index e68788f16..07db7d8cf 100644
--- a/packages/demobank-ui/src/stories.test.ts
+++ b/packages/demobank-ui/src/stories.test.ts
@@ -26,6 +26,7 @@ import * as pages from "./pages/index.stories.js";
import { ComponentChildren, VNode, h as create } from "preact";
import { BackendStateProviderTesting } from "./context/backend.js";
+import { AccessToken } from "./hooks/useCredentialsChecker.js";
setupI18n("en", { en: {} });
@@ -56,7 +57,7 @@ function DefaultTestingContext({
state: {
status: "loggedIn",
username: "test",
- password: "pwd",
+ token: "pwd" as AccessToken,
isUserAdministrator: false,
},
});
diff --git a/packages/demobank-ui/src/stories.tsx b/packages/demobank-ui/src/stories.tsx
index c6e8eb9ba..87848cb09 100644
--- a/packages/demobank-ui/src/stories.tsx
+++ b/packages/demobank-ui/src/stories.tsx
@@ -25,8 +25,6 @@ import * as components from "./components/index.examples.js";
import { renderStories } from "@gnu-taler/web-util/browser";
-import "./scss/main.scss";
-
function main(): void {
renderStories(
{ pages, components },
diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts
index 4ce0f140e..e7673f078 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -16,11 +16,12 @@
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
import {
+ ErrorNotification,
ErrorType,
HttpError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { ErrorMessage } from "./hooks/notification.js";
+
/**
* Validate (the number part of) an amount. If needed,
@@ -87,28 +88,6 @@ export enum CashoutStatus {
PENDING = "pending",
}
-// export function partialWithObjects<T extends object>(obj: T | undefined, () => complete): WithIntermediate<T> {
-// const root = obj === undefined ? {} : obj;
-// return Object.entries(root).([key, value]) => {
-
-// })
-// return undefined as any
-// }
-
-/**
- * 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;
-// }
export const PAGE_SIZE = 20;
export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
@@ -120,11 +99,12 @@ export function buildRequestErrorMessage(
onClientError?: (status: HttpStatusCode) => TranslatedString | undefined;
onServerError?: (status: HttpStatusCode) => TranslatedString | undefined;
} = {},
-): ErrorMessage {
- let result: ErrorMessage;
+): ErrorNotification {
+ let result: ErrorNotification;
switch (cause.type) {
case ErrorType.TIMEOUT: {
result = {
+ type: "error",
title: i18n.str`Request timeout`,
};
break;
@@ -133,8 +113,9 @@ export function buildRequestErrorMessage(
const title =
specialCases.onClientError && specialCases.onClientError(cause.status);
result = {
+ type: "error",
title: title ? title : i18n.str`The server didn't accept the request`,
- description: cause?.payload?.error?.description,
+ description: cause?.payload?.error?.description as TranslatedString,
debug: JSON.stringify(cause),
};
break;
@@ -143,24 +124,27 @@ export function buildRequestErrorMessage(
const title =
specialCases.onServerError && specialCases.onServerError(cause.status);
result = {
+ type: "error",
title: title
? title
: i18n.str`The server had problems processing the request`,
- description: cause?.payload?.error?.description,
+ description: cause?.payload?.error?.description as TranslatedString,
debug: JSON.stringify(cause),
};
break;
}
case ErrorType.UNREADABLE: {
result = {
+ type: "error",
title: i18n.str`Unexpected error`,
- description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}`,
+ description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}` as TranslatedString,
debug: JSON.stringify(cause),
};
break;
}
case ErrorType.UNEXPECTED: {
result = {
+ type: "error",
title: i18n.str`Unexpected error`,
debug: JSON.stringify(cause),
};
diff --git a/packages/demobank-ui/tailwind.config.js b/packages/demobank-ui/tailwind.config.js
new file mode 100644
index 000000000..01f058b2e
--- /dev/null
+++ b/packages/demobank-ui/tailwind.config.js
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ["./src/**/*.{html,tsx}"],
+ theme: {
+ extend: {},
+ },
+ plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
+};
diff --git a/packages/merchant-backoffice-ui/Makefile b/packages/merchant-backoffice-ui/Makefile
index 1f7e0bf2b..7175ef723 100644
--- a/packages/merchant-backoffice-ui/Makefile
+++ b/packages/merchant-backoffice-ui/Makefile
@@ -3,6 +3,7 @@
ifeq ($(TOPLEVEL), yes)
$(info top-level build)
-include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
else
$(info package-level build)
-include ../../.config.mk
@@ -15,7 +16,7 @@ $(info prefix is $(prefix))
all:
@echo run \'make install\' to install
-spa_dir=$(prefix)/share/taler/merchant-backoffice
+spa_dir=$(DESTDIR)$(prefix)/share/taler/merchant-backoffice
.PHONY: deps
deps:
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx
index 1a7617643..f0a7de53b 100644
--- a/packages/merchant-backoffice-ui/src/Application.tsx
+++ b/packages/merchant-backoffice-ui/src/Application.tsx
@@ -60,7 +60,7 @@ export function Application(): VNode {
* @returns
*/
function ApplicationStatusRoutes(): VNode {
- const { url: backendURL, updateToken, changeBackend } = useBackendContext();
+ const { changeBackend, selected: backendSelected } = useBackendContext();
const result = useBackendConfig();
const { i18n } = useTranslationContext();
@@ -69,7 +69,7 @@ function ApplicationStatusRoutes(): VNode {
: { currency: "unknown", version: "unknown" };
const ctx = useMemo(() => ({ currency, version }), [currency, version]);
- if (!backendURL) {
+ if (!backendSelected) {
return (
<Fragment>
<NotConnectedAppMenu title="Welcome!" />
diff --git a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
index 8bfbdb076..ebfa2b6d6 100644
--- a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
@@ -114,7 +114,7 @@ export function ApplicationReadyRoutes(): VNode {
<NotificationCard
notification={{
message: i18n.str`Access denied`,
- description: i18n.str`Check your token is valid 1`,
+ description: i18n.str`Check your token is valid`,
type: "ERROR",
}}
/>
diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
index c2a9d3b18..f5372db8d 100644
--- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
@@ -87,9 +87,9 @@ export enum InstancePaths {
bank_update = "/bank/:bid/update",
bank_new = "/bank/new",
- product_list = "/products",
- product_update = "/product/:pid/update",
- product_new = "/product/new",
+ inventory_list = "/inventory",
+ inventory_update = "/inventory/:pid/update",
+ inventory_new = "/inventory/new",
order_list = "/orders",
order_new = "/order/new",
@@ -347,42 +347,42 @@ export function InstanceRoutes({
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
/>
{/**
- * Product pages
+ * Inventory pages
*/}
<Route
- path={InstancePaths.product_list}
+ path={InstancePaths.inventory_list}
component={ProductListPage}
onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
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);
}}
/>
{/**
@@ -405,7 +405,7 @@ export function InstanceRoutes({
path={InstancePaths.bank_update}
component={BankAccountUpdatePage}
onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.product_list)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)}
onConfirm={() => {
route(InstancePaths.bank_list);
}}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
index 012d14977..1d18685c5 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
@@ -212,10 +212,7 @@ export function InputStock<T>({
withTimestampSupport
/>
- <InputGroup<Entity>
- name="address"
- label={i18n.str`Delivery address`}
- >
+ <InputGroup<Entity> name="address" label={i18n.str`Warehouse address`}>
<InputLocation name="address" />
</InputGroup>
</FormProvider>
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index 3d5f20c85..402134096 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -49,7 +49,7 @@ export function Sidebar({
isPasswordOk
}: Props): VNode {
const config = useConfigContext();
- const { url: backendURL } = useBackendContext()
+ const { url: backendURL, resetBackend } = useBackendContext()
const { i18n } = useTranslationContext();
const kycStatus = useInstanceKYCDetails();
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
@@ -80,7 +80,7 @@ export function Sidebar({
</div>
</div>
<div class="menu is-menu-main">
- {isPasswordOk && instance ? (
+ {instance ? (
<Fragment>
<ul class="menu-list">
<li>
@@ -94,12 +94,12 @@ export function Sidebar({
</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">
- <i18n.Translate>Products</i18n.Translate>
+ <i18n.Translate>Inventory</i18n.Translate>
</span>
</a>
</li>
@@ -243,7 +243,7 @@ export function Sidebar({
</span>
</div>
</li>
- {isPasswordOk && admin && !mimic && (
+ {admin && !mimic && (
<Fragment>
<p class="menu-label">
<i18n.Translate>Instances</i18n.Translate>
@@ -270,7 +270,7 @@ export function Sidebar({
</li>
</Fragment>
)}
- {isPasswordOk &&
+ {isPasswordOk ?
<li>
<a
class="has-icon is-state-info is-hoverable"
@@ -283,8 +283,21 @@ export function Sidebar({
<i18n.Translate>Log out</i18n.Translate>
</span>
</a>
- </li>
- }
+ </li> :
+ <li>
+ <a
+ class="has-icon is-state-info is-hoverable"
+ onClick={(): void => resetBackend()}
+ >
+ <span class="icon">
+ <i class="mdi mdi-logout default" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Change server</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ }
</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 cb318906f..b8ac2c9ab 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -30,11 +30,11 @@ function getInstanceTitle(path: string, id: string): string {
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`;
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
index 8bebbd298..e91e8c876 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
@@ -146,9 +146,9 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
/>
<InputNumber<Entity>
name="minimum_age"
- label={i18n.str`Age restricted`}
+ label={i18n.str`Age restriction`}
tooltip={i18n.str`is this product restricted for customer below certain age?`}
- help={i18n.str`can be overridden by the order configuration`}
+ help={i18n.str`minimum age of the buyer`}
/>
<Input<Entity>
name="unit"
@@ -165,7 +165,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
name="stock"
label={i18n.str`Stock`}
alreadyExist={alreadyExist}
- tooltip={i18n.str`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"
diff --git a/packages/merchant-backoffice-ui/src/context/backend.test.ts b/packages/merchant-backoffice-ui/src/context/backend.test.ts
index b042d5a25..ad6393e29 100644
--- a/packages/merchant-backoffice-ui/src/context/backend.test.ts
+++ b/packages/merchant-backoffice-ui/src/context/backend.test.ts
@@ -64,7 +64,7 @@ describe("backend context api ", () => {
} as MerchantBackend.Instances.QueryInstancesResponse,
});
- management.setNewToken("another_token" as AccessToken);
+ management.setNewAccessToken(undefined,"another_token" as AccessToken);
},
({ instance, management, admin }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
@@ -113,7 +113,7 @@ describe("backend context api ", () => {
name: "instance_name",
} as MerchantBackend.Instances.QueryInstancesResponse,
});
- instance.setNewToken("another_token" as AccessToken);
+ instance.setNewAccessToken(undefined, "another_token" as AccessToken);
},
({ instance, management, admin }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts
index 056f9a192..d4a9abd5f 100644
--- a/packages/merchant-backoffice-ui/src/context/backend.ts
+++ b/packages/merchant-backoffice-ui/src/context/backend.ts
@@ -20,35 +20,55 @@
*/
import { createContext, h, VNode } from "preact";
-import { useContext } from "preact/hooks";
+import { useContext, useState } from "preact/hooks";
import { LoginToken } from "../declaration.js";
import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { codecForBoolean } from "@gnu-taler/taler-util";
interface BackendContextType {
url: string,
+ selected: boolean;
token?: LoginToken;
updateToken: (token: LoginToken | undefined) => void;
changeBackend: (url: string) => void;
+ resetBackend: () => void;
}
const BackendContext = createContext<BackendContextType>({
url: "",
+ selected: false,
token: undefined,
updateToken: () => null,
changeBackend: () => null,
+ resetBackend: () => null,
});
+const BACKEND_SELECTED = buildStorageKey("backend-selected", codecForBoolean());
+
function useBackendContextState(
defaultUrl?: string,
): BackendContextType {
- const [url, changeBackend] = useBackendURL(defaultUrl);
+ const [url, changeBackend2] = useBackendURL(defaultUrl);
const [token, updateToken] = useBackendDefaultToken();
+ const {value, update} = useLocalStorage(BACKEND_SELECTED)
+
+ function changeBackend(s:string) {
+ changeBackend2(s)
+ update(true)
+ }
+
+ function resetBackend() {
+ update(false)
+ }
return {
url,
token,
+ selected: value ?? false,
updateToken,
- changeBackend
+ changeBackend,
+ resetBackend
};
}
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index c3e6ea3da..dc53e3e83 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -1327,7 +1327,7 @@ export namespace MerchantBackend {
otp_device_id: string;
// Human-readable description for the device.
- otp_description: string;
+ otp_device_description: string;
// A base64-encoded key
otp_key: string;
@@ -1341,7 +1341,7 @@ export namespace MerchantBackend {
interface OtpDevicePatchDetails {
// Human-readable description for the device.
- otp_description: string;
+ otp_device_description: string;
// A base64-encoded key
otp_key: string | undefined;
diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts
index fe4155788..eaeede103 100644
--- a/packages/merchant-backoffice-ui/src/hooks/backend.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts
@@ -266,7 +266,7 @@ export function useBackendBaseRequest(): useBackendBaseRequestType {
endpoint: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(backend, endpoint, { token, ...options }).then(res => {
+ return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => {
return res
}).catch(err => {
throw err
diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts
index ee696779f..498e4eb78 100644
--- a/packages/merchant-backoffice-ui/src/hooks/index.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/index.ts
@@ -31,7 +31,8 @@ const calculateRootPath = () => {
typeof window !== undefined
? window.location.origin + window.location.pathname
: "/";
- return rootPath;
+
+ return rootPath.replace("webui/","");
};
const loginTokenCodec = buildCodecForObject<LoginToken>()
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
index a7b8d047c..0c0c44590 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
@@ -158,7 +158,7 @@ describe("instance api interaction with details", () => {
},
} as MerchantBackend.Instances.QueryInstancesResponse,
});
- api.setNewToken("secret" as AccessToken);
+ api.setNewAccessToken(undefined, "secret" as AccessToken);
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
@@ -234,7 +234,7 @@ describe("instance api interaction with details", () => {
} as MerchantBackend.Instances.QueryInstancesResponse,
});
- api.clearToken();
+ api.clearAccessToken(undefined);
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts
index 50f9487a3..0677191db 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -36,8 +36,8 @@ interface InstanceAPI {
data: MerchantBackend.Instances.InstanceReconfigurationMessage,
) => Promise<void>;
deleteInstance: () => Promise<void>;
- clearToken: () => Promise<void>;
- setNewToken: (token: AccessToken) => Promise<void>;
+ clearAccessToken: (currentToken: AccessToken | undefined) => Promise<void>;
+ setNewAccessToken: (currentToken: AccessToken | undefined, token: AccessToken) => Promise<void>;
}
export function useAdminAPI(): AdminAPI {
@@ -111,18 +111,20 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
mutateAll(/\/management\/instances/);
};
- const clearToken = async (): Promise<void> => {
+ 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 setNewToken = async (newToken: AccessToken): Promise<void> => {
+ 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 },
});
@@ -137,7 +139,7 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
mutateAll(/\/management\/instances/);
};
- return { updateInstance, deleteInstance, setNewToken, clearToken };
+ return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken };
}
export function useInstanceAPI(): InstanceAPI {
@@ -172,18 +174,20 @@ export function useInstanceAPI(): InstanceAPI {
mutate([`/private/`], null);
};
- const clearToken = async (): Promise<void> => {
+ const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => {
await request(`/private/auth`, {
method: "POST",
+ token: currentToken,
data: { method: "external" },
});
mutate([`/private/`], null);
};
- const setNewToken = async (newToken: AccessToken): Promise<void> => {
+ const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => {
await request(`/private/auth`, {
method: "POST",
+ token: currentToken,
data: { method: "token", token: newToken },
});
@@ -198,7 +202,7 @@ export function useInstanceAPI(): InstanceAPI {
mutate([`/private/`], null);
};
- return { updateInstance, deleteInstance, setNewToken, clearToken };
+ return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken };
}
export function useInstanceDetails(): HttpResponse<
diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts
index 3544b4881..93eefeea5 100644
--- a/packages/merchant-backoffice-ui/src/hooks/otp.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts
@@ -30,13 +30,13 @@ const useSWR = _useSWR as unknown as SWRHook;
const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = {
"1": {
- otp_description: "first device",
+ otp_device_description: "first device",
otp_algorithm: 1,
otp_device_id: "1",
otp_key: "123",
},
"2": {
- otp_description: "second device",
+ otp_device_description: "second device",
otp_algorithm: 0,
otp_device_id: "2",
otp_key: "456",
diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts
index 8ecaefaa6..e06ea8ed8 100644
--- a/packages/merchant-backoffice-ui/src/hooks/product.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/product.ts
@@ -26,6 +26,9 @@ 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>;
@@ -66,7 +69,7 @@ export function useProductAPI(): ProductAPI {
data,
});
- return await mutateAll(/.*"\/private\/products.*/);
+ return await mutateAll(/.*\/private\/products.*/);
};
const deleteProduct = async (productId: string): Promise<void> => {
@@ -88,7 +91,17 @@ export function useProductAPI(): ProductAPI {
return await mutateAll(/.*"\/private\/products.*/);
};
- return { createProduct, updateProduct, deleteProduct, lockProduct };
+ 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<
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 cbfe1d573..db73217ed 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
@@ -66,7 +66,7 @@ export function CardTable({
<span class="icon">
<i class="mdi mdi-shopping" />
</span>
- <i18n.Translate>Products</i18n.Translate>
+ <i18n.Translate>Inventory</i18n.Translate>
</p>
<div class="card-header-icon" aria-label="more options">
<span
@@ -142,7 +142,7 @@ function Table({
<i18n.Translate>Taxes</i18n.Translate>
</th>
<th>
- <i18n.Translate>Profit</i18n.Translate>
+ <i18n.Translate>Sales</i18n.Translate>
</th>
<th>
<i18n.Translate>Stock</i18n.Translate>
@@ -190,18 +190,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={() =>
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 85c50e5ed..274a7c2ea 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
@@ -53,7 +53,7 @@ export default function ProductList({
onNotFound,
}: Props): VNode {
const result = useInstanceProducts();
- const { deleteProduct, updateProduct } = useProductAPI();
+ const { deleteProduct, updateProduct, getProduct } = useProductAPI();
const [deleting, setDeleting] =
useState<MerchantBackend.Products.ProductDetail & WithId | null>(null);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
@@ -74,11 +74,61 @@ export default function ProductList({
return onNotFound();
return onLoadError(result);
}
+ const [errorId, setErrorId] = useState<string | undefined>(
+ undefined,
+ );
+
+ const [productId, setProductId] = useState<string>()
+ async function testIfProductExistAndSelect(orderId: string | undefined): Promise<void> {
+ if (!orderId) {
+ setErrorId(i18n.str`Enter a product id`);
+ return;
+ }
+ try {
+ await getProduct(orderId);
+ onSelect(orderId);
+ setErrorId(undefined);
+ } catch {
+ setErrorId(i18n.str`product not found`);
+ }
+ }
return (
<section class="section is-main-section">
<NotificationCard notification={notif} />
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <div class="field has-addons">
+ <div class="control">
+ <input
+ class={errorId ? "input is-danger" : "input"}
+ type="text"
+ value={productId ?? ""}
+ onChange={(e) => setProductId(e.currentTarget.value)}
+ placeholder={i18n.str`product id`}
+ />
+ {errorId && <p class="help is-danger">{errorId}</p>}
+ </div>
+ <span
+ class="has-tooltip-bottom"
+ data-tooltip={i18n.str`jump to product with the given product ID`}
+ >
+ <button
+ class="button"
+ onClick={(e) => testIfProductExistAndSelect(productId)}
+ >
+ <span class="icon">
+ <i class="mdi mdi-arrow-right" />
+ </span>
+ </button>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+
<CardTable
instances={result.data}
onCreate={onCreate}
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
index 2201e75a5..0d2bb2c30 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+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";
@@ -35,6 +35,7 @@ import {
PAYTO_WIRE_METHOD_LOOKUP,
URL_REGEX,
} from "../../../../utils/constants.js";
+import { useBackendBaseRequest } from "../../../../hooks/backend.js";
type Entity = MerchantBackend.Rewards.ReserveCreateRequest;
@@ -65,6 +66,7 @@ function ViewStep({
setReserve,
}: ViewProps): VNode {
const { i18n } = useTranslationContext();
+ const {request} = useApiContext()
const [wireMethods, setWireMethods] = useState<Array<string>>([]);
const [exchangeQueryError, setExchangeQueryError] = useState<
string | undefined
@@ -123,19 +125,26 @@ function ViewStep({
<AsyncButton
class="has-tooltip-left"
onClick={() => {
- return fetch(`${reserve.exchange_url}wire`)
- .then((r) => r.json())
+ if (!reserve.exchange_url) {
+ return Promise.resolve();
+ }
+
+ return request<any>(reserve.exchange_url, "keys") //</div>fetch(`${reserve.exchange_url}wire`)
.then((r) => {
- const wireMethods = r.accounts.map((a: any) => {
- const match = PAYTO_WIRE_METHOD_LOOKUP.exec(a.payto_uri);
- return (match && match[1]) || "";
- });
+ if (r.loading) return;
+ if (r.ok) {
+ const wireMethods = r.data.accounts.map((a: any) => {
+ 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);
+ .catch((r: RequestError<{}>) => {
+ console.log(r.cause)
+ setExchangeQueryError(r.cause.message);
});
}}
data-tooltip={
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
index 4b0db200a..89dba63b2 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
@@ -30,13 +30,13 @@ import { AccessToken } from "../../../declaration.js";
interface Props {
instanceId: string;
- currentToken: string | undefined;
- onClearToken: () => void;
- onNewToken: (s: AccessToken) => void;
+ hasToken: boolean | undefined;
+ onClearToken: (c: AccessToken | undefined) => void;
+ onNewToken: (c: AccessToken | undefined, s: AccessToken) => void;
onBack?: () => void;
}
-export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewToken, onClearToken }: Props): VNode {
+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: "",
@@ -45,11 +45,9 @@ export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewTo
});
const { i18n } = useTranslationContext();
- const hasOldtoken = !!oldToken
- const hasInputTheCorrectOldToken = hasOldtoken && oldToken !== form.old_token;
const errors = {
- old_token: hasInputTheCorrectOldToken
- ? i18n.str`is not the same as the current access token`
+ 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`
@@ -72,8 +70,9 @@ export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewTo
async function submitForm() {
if (hasErrors) return;
+ const ot = hasToken ? `secret-token:${form.old_token}` as AccessToken : undefined;
const nt = `secret-token:${form.new_token}` as AccessToken;
- onNewToken(nt)
+ onNewToken(ot, nt)
}
return (
@@ -98,32 +97,38 @@ export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewTo
<div class="column" />
<div class="column is-four-fifths">
<FormProvider errors={errors} object={form} valueHandler={setValue}>
- {hasOldtoken && (
- <Input<State>
- name="old_token"
- label={i18n.str`Current access token`}
- tooltip={i18n.str`access token currently in use`}
- inputType="password"
- />
- )}
- {!hasInputTheCorrectOldToken && <Fragment>
- {hasOldtoken && <Fragment>
- <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
- disabled={!!hasInputTheCorrectOldToken}
- class="button"
- onClick={onClearToken}
- >
- <i18n.Translate>Clear token</i18n.Translate>
- </button>
- </div>
- </Fragment>
- }
+ <Fragment>
+ {hasToken && (
+ <Fragment>
+ <Input<State>
+ name="old_token"
+ label={i18n.str`Current access token`}
+ tooltip={i18n.str`access token currently in use`}
+ inputType="password"
+ />
+ <p>
+ <i18n.Translate>
+ Clearing the access token will mean public access to the instance.
+ </i18n.Translate>
+ </p>
+ <div class="buttons is-right mt-5">
+ <button
+ class="button"
+ onClick={() => {
+ if (hasToken) {
+ const ot = `secret-token:${form.old_token}` as AccessToken;
+ onClearToken(ot)
+ } else {
+ onClearToken(undefined)
+ }
+ }}
+ >
+ <i18n.Translate>Clear token</i18n.Translate>
+ </button>
+ </div>
+ </Fragment>
+ )}
+
<Input<State>
name="new_token"
@@ -137,7 +142,7 @@ export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewTo
tooltip={i18n.str`confirm the same access token`}
inputType="password"
/>
- </Fragment>}
+ </Fragment>
</FormProvider>
<div class="buttons is-right mt-5">
{onBack && (
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
index 0a49448f8..bc2bd9fa3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
@@ -33,8 +33,6 @@ interface Props {
onNotFound: () => VNode;
}
-const PREFIX = "secret-token:"
-
export default function Token({
onLoadError,
onChange,
@@ -44,21 +42,36 @@ export default function Token({
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { clearToken, setNewToken } = useInstanceAPI();
- const { token: rootToken } = useBackendContext();
- const { token: instanceToken, id, admin } = useInstanceContext();
+ 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"
- const currentToken = !admin ? rootToken : instanceToken
- const hasPrefix = currentToken !== undefined && currentToken.token.startsWith(PREFIX)
return (
<Fragment>
<NotificationCard notification={notif} />
<DetailPage
instanceId={id}
- currentToken={hasPrefix ? currentToken.token.substring(PREFIX.length) : currentToken?.token}
- onClearToken={async (): Promise<void> => {
+ hasToken={hasToken}
+ onClearToken={async (currentToken): Promise<void> => {
try {
- await clearToken();
+ await clearAccessToken(currentToken);
onChange();
} catch (error) {
if (error instanceof Error) {
@@ -70,9 +83,9 @@ export default function Token({
}
}
}}
- onNewToken={async (newToken): Promise<void> => {
+ onNewToken={async (currentToken, newToken): Promise<void> => {
try {
- await setNewToken(newToken);
+ await setNewAccessToken(currentToken, newToken);
onChange();
} catch (error) {
if (error instanceof Error) {
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 a1c608f15..01a3d0252 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
@@ -39,9 +39,6 @@ type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & {
//MerchantBackend.Instances.InstanceAuthConfigurationMessage
interface Props {
onUpdate: (d: Entity) => void;
- onChangeAuth: (
- d: MerchantBackend.Instances.InstanceAuthConfigurationMessage,
- ) => Promise<void>;
selected: MerchantBackend.Instances.QueryInstancesResponse;
isLoading: boolean;
onBack: () => void;
@@ -78,7 +75,6 @@ function getTokenValuePart(t?: string): string | undefined {
export function UpdatePage({
onUpdate,
- onChangeAuth,
selected,
onBack,
}: Props): VNode {
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 6c5e7a514..e44cf5c0f 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
@@ -46,17 +46,17 @@ export interface Props {
}
export default function Update(props: Props): VNode {
- const { updateInstance, clearToken, setNewToken } = useInstanceAPI();
+ const { updateInstance } = useInstanceAPI();
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(
+ const { updateInstance } = useManagementAPI(
props.instanceId,
);
const result = useManagedInstanceDetails(props.instanceId);
- return CommonUpdate(props, result, updateInstance, clearToken, setNewToken);
+ return CommonUpdate(props, result, updateInstance, );
}
function CommonUpdate(
@@ -73,8 +73,6 @@ function CommonUpdate(
MerchantBackend.ErrorDetail
>,
updateInstance: any,
- clearToken: () => Promise<void>,
- setNewToken: (t: AccessToken) => Promise<void>,
): VNode {
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -114,13 +112,6 @@ function CommonUpdate(
}),
);
}}
- onChangeAuth={(
- d: MerchantBackend.Instances.InstanceAuthConfigurationMessage,
- ): Promise<void> => {
- const apiCall =
- d.method === "external" ? clearToken() : setNewToken(d.token! as AccessToken);
- return apiCall.then(onConfirm).catch(onUpdateError);
- }}
/>
</Fragment>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx
index bdc86d226..cebc1ade6 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx
@@ -70,8 +70,8 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
: state.otp_key.length !== 32
? i18n.str`size of the key should be 32`
: undefined,
- otp_description: !state.otp_description ? i18n.str`required`
- : !/[a-zA-Z0-9]*/.test(state.otp_description)
+ 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,
@@ -103,7 +103,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
tooltip={i18n.str`Internal id on the system`}
/>
<Input<Entity>
- name="otp_description"
+ name="otp_device_description"
label={i18n.str`Descripiton`}
tooltip={i18n.str`Useful to identify the device physically`}
/>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx
index 22ae55677..db3842711 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx
@@ -77,7 +77,7 @@ export function CreatedSuccessfully({
<input
class="input"
readonly
- value={entity.otp_description}
+ value={entity.otp_device_description}
/>
</p>
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx
index 585c12e11..79be9802f 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx
@@ -87,7 +87,7 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
errors={errors}
>
<Input<Entity>
- name="otp_description"
+ name="otp_device_description"
label={i18n.str`Description`}
tooltip={i18n.str`dddd`}
/>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx
index 9a27ccfee..52f6c6c29 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx
@@ -80,7 +80,7 @@ export default function UpdateValidator({
device={{
id: vid,
otp_algorithm: result.data.otp_algorithm,
- otp_description: result.data.device_description,
+ otp_device_description: result.data.device_description,
otp_key: undefined,
otp_ctr: result.data.otp_ctr
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
index 9948307e4..a9e3c3a1b 100644
--- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
@@ -52,7 +52,7 @@ function cleanUp(s: string): string {
}
export function LoginPage({ onConfirm }: Props): VNode {
- const { url: backendURL, changeBackend } = useBackendContext();
+ const { url: backendURL, changeBackend, resetBackend } = useBackendContext();
const { admin, id } = useInstanceContext();
const { requestNewLoginToken } = useCredentialsChecker();
const [token, setToken] = useState("");
@@ -73,10 +73,9 @@ export function LoginPage({ onConfirm }: Props): VNode {
}, [backendURL, id, token])
async function changeServer() {
- changeBackend("")
+ resetBackend()
}
- console.log(admin, id)
if (admin && id !== "default") {
//admin trying to access another instance
return (<div class="columns is-centered" style={{ margin: "auto" }}>
@@ -211,10 +210,7 @@ export function LoginPage({ onConfirm }: Props): VNode {
borderTop: 0,
}}
>
- <AsyncButton
-
- onClick={changeServer}
- >
+ <AsyncButton onClick={changeServer}>
<i18n.Translate>Change server</i18n.Translate>
</AsyncButton>
@@ -304,11 +300,8 @@ export function ConnectionPage({ onConfirm }: { onConfirm: (s: string) => void }
borderTop: 0,
}}
>
- <AsyncButton
- disabled={backendURL === url}
- onClick={doConnect}
- >
- <i18n.Translate>Try again</i18n.Translate>
+ <AsyncButton onClick={doConnect}>
+ <i18n.Translate>Connect</i18n.Translate>
</AsyncButton>
</footer>
</div>
diff --git a/packages/merchant-backoffice-ui/src/schemas/index.ts b/packages/merchant-backoffice-ui/src/schemas/index.ts
index 4be77595b..c97d41204 100644
--- a/packages/merchant-backoffice-ui/src/schemas/index.ts
+++ b/packages/merchant-backoffice-ui/src/schemas/index.ts
@@ -22,6 +22,7 @@
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: {
@@ -38,7 +39,7 @@ function listOfPayToUrisAreValid(values?: (string | undefined)[]): boolean {
}
function currencyWithAmountIsValid(value?: string): boolean {
- return !!value && AMOUNT_REGEX.test(value);
+ return !!value && Amounts.parse(value) !== undefined;
}
function currencyGreaterThan0(value?: string) {
if (value) {
diff --git a/packages/merchant-backoffice-ui/src/utils/constants.ts b/packages/merchant-backoffice-ui/src/utils/constants.ts
index fea9cb7e2..7c4e288b3 100644
--- a/packages/merchant-backoffice-ui/src/utils/constants.ts
+++ b/packages/merchant-backoffice-ui/src/utils/constants.ts
@@ -25,7 +25,7 @@ export const PAYTO_REGEX =
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 AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]{1,11}:[0-9][0-9,]*\.?[0-9,]*$/;
export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/;
diff --git a/packages/taler-harness/Makefile b/packages/taler-harness/Makefile
index ed8365dc8..f8bad0f99 100644
--- a/packages/taler-harness/Makefile
+++ b/packages/taler-harness/Makefile
@@ -3,6 +3,7 @@
ifeq ($(TOPLEVEL), yes)
$(info top-level build)
-include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
else
$(info package-level build)
-include ../../.config.mk
@@ -20,19 +21,20 @@ warn-noprefix:
@echo "no prefix configured, did you run ./configure?"
install: warn-noprefix
else
-install_target = $(prefix)/lib/taler-harness
+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 $(prefix)/bin
- install -d $(install_target)/bin
- install -d $(install_target)/node_modules/taler-harness
- install -d $(install_target)/node_modules/taler-harness/bin
- install -d $(install_target)/node_modules/taler-harness/dist
- install ./dist/taler-harness-bundled.cjs $(install_target)/node_modules/taler-harness/dist/
- install ./dist/taler-harness-bundled.cjs.map $(install_target)/node_modules/taler-harness/dist/
- install ./bin/taler-harness.mjs $(install_target)/node_modules/taler-harness/bin/
- ln -sf $(install_target)/node_modules/taler-harness/bin/taler-harness.mjs $(prefix)/bin/taler-harness
+ 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:
diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json
index 38f168af4..b4896916b 100644
--- a/packages/taler-harness/package.json
+++ b/packages/taler-harness/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-harness",
- "version": "0.9.3-dev.17",
+ "version": "0.9.3-dev.27",
"description": "",
"engines": {
"node": ">=0.12.0"
@@ -42,4 +42,4 @@
"@gnu-taler/taler-wallet-core": "workspace:*",
"tslib": "^2.5.3"
}
-}
+} \ No newline at end of file
diff --git a/packages/taler-harness/src/bench1.ts b/packages/taler-harness/src/bench1.ts
index 618eb683e..efe162320 100644
--- a/packages/taler-harness/src/bench1.ts
+++ b/packages/taler-harness/src/bench1.ts
@@ -96,10 +96,10 @@ export async function runBench1(configJson: any): Promise<void> {
logger.trace(`Starting withdrawal amount=${withdrawAmount}`);
let start = Date.now();
- await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
amount: b1conf.currency + ":" + withdrawAmount,
- bank: b1conf.bank,
- exchange: b1conf.exchange,
+ corebankApiBaseUrl: b1conf.bank,
+ exchangeBaseUrl: b1conf.exchange,
});
await wallet.runTaskLoop({
diff --git a/packages/taler-harness/src/bench3.ts b/packages/taler-harness/src/bench3.ts
index 0b5371af5..bc345aa9e 100644
--- a/packages/taler-harness/src/bench3.ts
+++ b/packages/taler-harness/src/bench3.ts
@@ -107,10 +107,10 @@ export async function runBench3(configJson: any): Promise<void> {
logger.trace(`Starting withdrawal amount=${withdrawAmount}`);
let start = Date.now();
- await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
amount: b3conf.currency + ":" + withdrawAmount,
- bank: b3conf.bank,
- exchange: b3conf.exchange,
+ corebankApiBaseUrl: b3conf.bank,
+ exchangeBaseUrl: b3conf.exchange,
});
await wallet.runTaskLoop({
diff --git a/packages/taler-harness/src/env-full.ts b/packages/taler-harness/src/env-full.ts
index 210d38e32..bb2cb8c47 100644
--- a/packages/taler-harness/src/env-full.ts
+++ b/packages/taler-harness/src/env-full.ts
@@ -25,7 +25,7 @@ import {
ExchangeService,
FakebankService,
MerchantService,
- getPayto,
+ generateRandomPayto,
} from "./harness/harness.js";
/**
@@ -82,7 +82,7 @@ export async function runEnvFull(t: GlobalTestState): Promise<void> {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -91,7 +91,7 @@ export async function runEnvFull(t: GlobalTestState): Promise<void> {
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 0c3c367af..e30cbcb54 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -28,7 +28,7 @@ import {
AccountAddDetails,
AmountJson,
Amounts,
- BankAccessApiClient,
+ TalerCorebankApiClient,
Configuration,
CoreApiResponse,
Duration,
@@ -565,7 +565,7 @@ class BankServiceBase {
protected globalTestState: GlobalTestState,
protected bankConfig: BankConfig,
protected configFile: string,
- ) { }
+ ) {}
}
export interface HarnessExchangeBankAccount {
@@ -580,7 +580,8 @@ export interface HarnessExchangeBankAccount {
*/
export class FakebankService
extends BankServiceBase
- implements BankServiceHandle {
+ implements BankServiceHandle
+{
proc: ProcessWrapper | undefined;
http = createPlatformHttpLib({ enableThrottling: false });
@@ -649,9 +650,8 @@ export class FakebankService
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;
}
async createExchangeAccount(
@@ -665,8 +665,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/`,
};
}
@@ -691,23 +691,157 @@ export class FakebankService
"bank",
);
await this.pingUntilAvailable();
- const bankClient = new BankAccessApiClient(this.bankAccessApiBaseUrl);
+ 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}/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 "taler-fakebank-run" tool.
+ */
+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_CUSTOMER_DEBT_LIMIT",
+ `${bc.currency}:500`,
+ );
+ config.setString(
+ "libeufin-bank",
+ "DEFAULT_ADMIN_DEBT_LIMIT",
+ `${bc.currency}:999999`,
+ );
+ config.setString(
+ "libeufin-bank",
+ "registration_bonus",
+ `${bc.currency}:100`,
+ );
+ config.setString("libeufin-bank", "registration_bonus_enabled", `yes`);
+ config.setString("libeufin-bank", "max_auth_token_duration", "1h");
+ 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_CUSTOMER_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}"`,
+ );
+
+ 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 bankAccessApiBaseUrl: string;
+ readonly corebankApiBaseUrl: string;
readonly http: HttpRequestLibrary;
}
@@ -1012,7 +1146,7 @@ export class ExchangeService implements ExchangeServiceInterface {
private exchangeConfig: ExchangeConfig,
private configFilename: string,
private keyPair: EddsaKeyPair,
- ) { }
+ ) {}
get name() {
return this.exchangeConfig.name;
@@ -1368,7 +1502,7 @@ export class MerchantService implements MerchantServiceInterface {
private globalState: GlobalTestState,
private merchantConfig: MerchantConfig,
private configFilename: string,
- ) { }
+ ) {}
private currentTimetravelOffsetMs: number | undefined;
@@ -1496,7 +1630,7 @@ export class MerchantService implements MerchantServiceInterface {
return await this.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
@@ -1659,6 +1793,7 @@ export async function runTestWithState(
e.message,
`error detail: ${j2s(e.errorDetail)}`,
);
+ console.error(e.stack);
} else {
console.error("FATAL: test failed with exception", e);
}
@@ -1706,7 +1841,7 @@ export class WalletService {
constructor(
private globalState: GlobalTestState,
private opts: WalletServiceOptions,
- ) { }
+ ) {}
get socketPath() {
const unixPath = path.join(
@@ -1815,7 +1950,7 @@ export class WalletClient {
return client.call(operation, payload);
}
- constructor(private args: WalletClientArgs) { }
+ constructor(private args: WalletClientArgs) {}
async connect(): Promise<void> {
const waiter = this.waiter;
@@ -1882,9 +2017,11 @@ export class WalletCli {
? `--crypto-worker=${cliOpts.cryptoWorkerType}`
: "";
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 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);
@@ -1967,7 +2104,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));
@@ -1999,9 +2136,9 @@ 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}`;
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
index 9892e600b..8c1612457 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -25,7 +25,7 @@
*/
import {
AmountString,
- BankAccessApiClient,
+ TalerCorebankApiClient,
ConfirmPayResultType,
Duration,
Logger,
@@ -56,7 +56,7 @@ import {
WalletClient,
WalletService,
WithAuthorization,
- getPayto,
+ generateRandomPayto,
setupDb,
setupSharedDb,
} from "./harness.js";
@@ -236,7 +236,7 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -245,7 +245,7 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -368,7 +368,7 @@ export async function createSimpleTestkudosEnvironmentV2(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -377,7 +377,7 @@ export async function createSimpleTestkudosEnvironmentV2(
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -512,13 +512,13 @@ export async function createFaultInjectedMerchantTestkudosEnvironment(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
@@ -560,7 +560,7 @@ export async function withdrawViaBankV2(
): Promise<WithdrawViaBankResult> {
const { walletClient: wallet, bank, exchange, amount } = p;
- const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl);
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
const user = await bankClient.createRandomBankUser();
const wop = await bankClient.createWithdrawalOperation(user.username, amount);
@@ -592,7 +592,9 @@ export async function withdrawViaBankV2(
// Confirm it
- await bankClient.confirmWithdrawalOperation(user.username, wop);
+ await bankClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
return {
withdrawalFinishedCond,
diff --git a/packages/taler-harness/src/harness/libeufin-apis.ts b/packages/taler-harness/src/harness/libeufin-apis.ts
deleted file mode 100644
index 0193f9252..000000000
--- a/packages/taler-harness/src/harness/libeufin-apis.ts
+++ /dev/null
@@ -1,775 +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 { URL } from "@gnu-taler/taler-util";
-import {
- createPlatformHttpLib,
- makeBasicAuthHeader,
-} from "@gnu-taler/taler-util/http";
-import {
- LibeufinNexusTransactions,
- LibeufinSandboxAdminBankAccountBalance,
- NexusBankConnections,
- NexusFacadeListResponse,
- NexusGetPermissionsResponse,
- NexusNewTransactionsInfo,
- NexusTask,
- NexusTaskCollection,
- NexusUserResponse,
-} from "./libeufin.js";
-
-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;
-}
-
-const libeufinHarnessHttpLib = createPlatformHttpLib();
-
-/**
- * APIs spread across Legacy and Access, it is therefore
- * the "base URL" relative to which API every call addresses.
- */
-export namespace LibeufinSandboxApi {
- // Creates one bank account via the Access API.
- // Need the /demobanks/$id/access-api as the base URL
- export async function createDemobankAccount(
- username: string,
- password: string,
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- iban: string | null = null,
- ): Promise<void> {
- let url = new URL("testing/register", libeufinSandboxService.baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- body: {
- username: username,
- password: password,
- iban: iban,
- },
- });
- }
- // Need /demobanks/$id as the base URL
- export async function createDemobankEbicsSubscriber(
- req: CreateEbicsSubscriberRequest,
- demobankAccountLabel: string,
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- username: string = "admin",
- password: string = "secret",
- ): Promise<void> {
- // baseUrl should already be pointed to one demobank.
- let url = new URL("ebics/subscribers", libeufinSandboxService.baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- body: {
- userID: req.userID,
- hostID: req.hostID,
- partnerID: req.partnerID,
- demobankAccountLabel: demobankAccountLabel,
- },
- });
- }
-
- export async function rotateKeys(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- hostID: string,
- ): Promise<void> {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL(`admin/ebics/hosts/${hostID}/rotate-keys`, baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- body: {},
- });
- }
- export async function createEbicsHost(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- hostID: string,
- ): Promise<void> {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL("admin/ebics/hosts", baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- body: {
- hostID,
- ebicsVersion: "2.5",
- },
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- }
-
- export async function createBankAccount(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- req: BankAccountInfo,
- ): Promise<void> {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL(`admin/bank-accounts/${req.label}`, baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- body: req,
- headers: { Authorization: makeBasicAuthHeader("admin", "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,
- ): Promise<void> {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL("admin/ebics/subscribers", baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- body: req,
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- }
-
- /**
- * Create a new bank account and associate it to
- * a existing EBICS subscriber.
- */
- export async function createEbicsBankAccount(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- req: CreateEbicsBankAccountRequest,
- ): Promise<void> {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL("admin/ebics/bank-accounts", baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- body: req,
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- }
-
- export async function simulateIncomingTransaction(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- accountLabel: string,
- req: SimulateIncomingTransactionRequest,
- ): Promise<void> {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL(
- `admin/bank-accounts/${accountLabel}/simulate-incoming-transaction`,
- baseUrl,
- );
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- body: req,
- headers: { Authorization: makeBasicAuthHeader("admin", "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 libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- return (await res.json()) 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 libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: {
- bankaccount: accountLabel,
- type: 53,
- },
- });
- }
-
- export async function getAccountInfoWithBalance(
- libeufinSandboxService: LibeufinSandboxServiceInterface,
- accountLabel: string,
- ): Promise<LibeufinSandboxAdminBankAccountBalance> {
- const baseUrl = libeufinSandboxService.baseUrl;
- let url = new URL(`admin/bank-accounts/${accountLabel}`, baseUrl);
- const res = await libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- return res.json();
- }
-}
-
-export namespace LibeufinNexusApi {
- export async function getAllConnections(
- nexus: LibeufinNexusServiceInterface,
- ): Promise<NexusBankConnections> {
- let url = new URL("bank-connections", nexus.baseUrl);
- const res = await libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- return res.json();
- }
-
- export async function deleteBankConnection(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: DeleteBankConnectionRequest,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL("bank-connections/delete-connection", baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: req,
- });
- }
-
- export async function createEbicsBankConnection(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: CreateEbicsBankConnectionRequest,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL("bank-connections", baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: {
- source: "new",
- type: "ebics",
- name: req.name,
- data: {
- ebicsURL: req.ebicsURL,
- hostID: req.hostID,
- userID: req.userID,
- partnerID: req.partnerID,
- systemID: req.systemID,
- },
- },
- });
- }
-
- export async function getBankAccount(
- libeufinNexusService: LibeufinNexusServiceInterface,
- accountName: string,
- ): Promise<any> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`bank-accounts/${accountName}`, baseUrl);
- const resp = await libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- return resp.json();
- }
-
- 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 libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: {},
- });
- }
-
- 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 libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: {},
- });
- }
-
- 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 libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: {
- offeredAccountId,
- nexusBankAccountId,
- },
- });
- }
-
- export async function connectBankConnection(
- libeufinNexusService: LibeufinNexusServiceInterface,
- connectionName: string,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`bank-connections/${connectionName}/connect`, baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: {},
- });
- }
-
- 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 libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- const respJson = await response.json();
- console.log(
- `Payment initiations of: ${accountName}`,
- JSON.stringify(respJson, null, 2),
- );
- }
-
- // Uses the Anastasis API to get a list of transactions.
- export async function getAnastasisTransactions(
- libeufinNexusService: LibeufinNexusServiceInterface,
- anastasisBaseUrl: string,
- // FIXME: Nail down type!
- params: {}, // of the request: {delta: 5, ..}
- username: string = "admin",
- password: string = "test",
- ): Promise<any> {
- let url = new URL("history/incoming", anastasisBaseUrl);
- for (const [k, v] of Object.entries(params)) {
- url.searchParams.set(k, String(v));
- }
- let response = await libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- return response.json();
- }
-
- // 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<LibeufinNexusTransactions> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/bank-accounts/${accountName}/transactions`, baseUrl);
- let response = await libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- return response.json();
- }
-
- export async function fetchTransactions(
- libeufinNexusService: LibeufinNexusServiceInterface,
- accountName: string,
- rangeType: string = "all",
- level: string = "report",
- username: string = "admin",
- password: string = "test",
- ): Promise<NexusNewTransactionsInfo> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `/bank-accounts/${accountName}/fetch-transactions`,
- baseUrl,
- );
- const resp = await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: {
- rangeType: rangeType,
- level: level,
- },
- });
- return resp.json();
- }
-
- export async function changePassword(
- libeufinNexusService: LibeufinNexusServiceInterface,
- username: string,
- req: UpdateNexusUserRequest,
- auth: NexusAuth,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/users/${username}/password`, baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: req,
- });
- }
-
- export async function getUser(
- libeufinNexusService: LibeufinNexusServiceInterface,
- auth: NexusAuth,
- ): Promise<NexusUserResponse> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/user`, baseUrl);
- const resp = await libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- return resp.json();
- }
-
- export async function createUser(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: CreateNexusUserRequest,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/users`, baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: req,
- });
- }
-
- export async function getAllPermissions(
- libeufinNexusService: LibeufinNexusServiceInterface,
- ): Promise<NexusGetPermissionsResponse> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/permissions`, baseUrl);
- const resp = await libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- return resp.json();
- }
-
- export async function postPermission(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: PostNexusPermissionRequest,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/permissions`, baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: req,
- });
- }
-
- export async function getAllTasks(
- libeufinNexusService: LibeufinNexusServiceInterface,
- bankAccountName: string,
- ): Promise<NexusTaskCollection> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl);
- const resp = await libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- return resp.json();
- }
-
- export async function getTask(
- libeufinNexusService: LibeufinNexusServiceInterface,
- bankAccountName: string,
- // When void, the request returns the list of all the
- // tasks under this bank account.
- taskName: string,
- ): Promise<NexusTask> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `/bank-accounts/${bankAccountName}/schedule/${taskName}`,
- baseUrl,
- );
- if (taskName) url = new URL(taskName, `${url.href}/`);
- const resp = await libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- return resp.json();
- }
-
- export async function deleteTask(
- libeufinNexusService: LibeufinNexusServiceInterface,
- bankAccountName: string,
- taskName: string,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `/bank-accounts/${bankAccountName}/schedule/${taskName}`,
- baseUrl,
- );
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "DELETE",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- }
-
- export async function postTask(
- libeufinNexusService: LibeufinNexusServiceInterface,
- bankAccountName: string,
- req: PostNexusTaskRequest,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: req,
- });
- }
-
- export async function deleteFacade(
- libeufinNexusService: LibeufinNexusServiceInterface,
- facadeName: string,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(`facades/${facadeName}`, baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "DELETE",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- }
-
- export async function getAllFacades(
- libeufinNexusService: LibeufinNexusServiceInterface,
- ): Promise<NexusFacadeListResponse> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL("facades", baseUrl);
- const resp = await libeufinHarnessHttpLib.fetch(url.href, {
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- });
- // FIXME: Just return validated, typed response here!
- return resp.json();
- }
-
- export async function createAnastasisFacade(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: CreateAnastasisFacadeRequest,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL("facades", baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: {
- name: req.name,
- type: "anastasis",
- config: {
- bankAccount: req.accountName,
- bankConnection: req.connectionName,
- currency: req.currency,
- reserveTransferLevel: req.reserveTransferLevel,
- },
- },
- });
- }
-
- export async function createTwgFacade(
- libeufinNexusService: LibeufinNexusServiceInterface,
- req: CreateTalerWireGatewayFacadeRequest,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL("facades", baseUrl);
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: {
- name: req.name,
- type: "taler-wire-gateway",
- config: {
- bankAccount: req.accountName,
- bankConnection: req.connectionName,
- currency: req.currency,
- reserveTransferLevel: req.reserveTransferLevel,
- },
- },
- });
- }
-
- export async function submitAllPaymentInitiations(
- libeufinNexusService: LibeufinNexusServiceInterface,
- accountId: string,
- ): Promise<void> {
- const baseUrl = libeufinNexusService.baseUrl;
- let url = new URL(
- `/bank-accounts/${accountId}/submit-all-payment-initiations`,
- baseUrl,
- );
- await libeufinHarnessHttpLib.fetch(url.href, {
- method: "POST",
- headers: { Authorization: makeBasicAuthHeader("admin", "secret") },
- body: {},
- });
- }
-}
diff --git a/packages/taler-harness/src/harness/libeufin.ts b/packages/taler-harness/src/harness/libeufin.ts
deleted file mode 100644
index caeea85ae..000000000
--- a/packages/taler-harness/src/harness/libeufin.ts
+++ /dev/null
@@ -1,1047 +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 { AmountString, Logger } from "@gnu-taler/taler-util";
-import {
- DbInfo,
- GlobalTestState,
- ProcessWrapper,
- getRandomIban,
- pingProc,
- runCommand,
- setupDb,
- sh,
-} from "../harness/harness.js";
-import {
- CreateAnastasisFacadeRequest,
- CreateEbicsBankAccountRequest,
- CreateEbicsBankConnectionRequest,
- CreateNexusUserRequest,
- CreateTalerWireGatewayFacadeRequest,
- LibeufinNexusApi,
- LibeufinSandboxApi,
- LibeufinSandboxServiceInterface,
- PostNexusPermissionRequest,
-} from "../harness/libeufin-apis.js";
-
-const logger = new Logger("libeufin.ts");
-
-export { LibeufinNexusApi, LibeufinSandboxApi };
-
-export interface LibeufinServices {
- libeufinSandbox: LibeufinSandboxService;
- libeufinNexus: LibeufinNexusService;
- commonDb: DbInfo;
-}
-
-export interface LibeufinSandboxConfig {
- httpPort: number;
- databaseJdbcUri: string;
-}
-
-export interface LibeufinNexusConfig {
- httpPort: number;
- databaseJdbcUri: string;
-}
-
-export 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;
- };
-}
-
-export interface LibeufinNexusBatches {
- batchTransactions: Array<LibeufinNexusMoneyMovement>;
-}
-
-export interface LibeufinNexusTransaction {
- amount: string;
- creditDebitIndicator: string;
- status: string;
- bankTransactionCode: string;
- valueDate: string;
- bookingDate: string;
- accountServicerRef: string;
- batches: Array<LibeufinNexusBatches>;
-}
-
-export interface LibeufinNexusTransactions {
- transactions: Array<LibeufinNexusTransaction>;
-}
-
-export interface LibeufinCliDetails {
- nexusUrl: string;
- sandboxUrl: string;
- nexusDatabaseUri: string;
- sandboxDatabaseUri: string;
- nexusUser: 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 interface NexusBankConnection {
- // connection type. For example "ebics".
- type: string;
-
- // connection name as given by the user at
- // the moment of creation.
- name: string;
-}
-
-export interface NexusBankConnections {
- bankConnections: NexusBankConnection[];
-}
-
-export interface FacadeShowInfo {
- // Name of the facade, same as the "fcid" parameter.
- name: string;
-
- // Type of the facade.
- // For example, "taler-wire-gateway".
- type: string;
-
- // Bas URL of the facade.
- baseUrl: string;
-
- // details depending on the facade type.
- config: any;
-}
-
-export interface FetchParams {
- // Because transactions are delivered by banks in "batches",
- // then every batch can have different qualities. This value
- // lets the request specify which type of batch ought to be
- // returned. Currently, the following two type are supported:
- //
- // 'report': typically includes only non booked transactions.
- // 'statement': typically includes only booked transactions.
- level: "report" | "statement" | "all";
-
- // This type indicates the time range of the query.
- // It allows the following values:
- //
- // 'latest': retrieves the last transactions from the bank.
- // If there are older unread transactions, those will *not*
- // be downloaded.
- //
- // 'all': retrieves all the transactions from the bank,
- // until the oldest.
- //
- // 'previous-days': currently *not* implemented, it will allow
- // the request to download transactions from
- // today until N days before.
- //
- // 'since-last': retrieves all the transactions since the last
- // time one was downloaded.
- //
- rangeType: "latest" | "all" | "previous-days" | "since-last";
-}
-
-export interface NexusTask {
- // The resource being impacted by this operation.
- // Typically a (Nexus) bank account being fetched
- // or whose payments are submitted. In this cases,
- // this value is the "bank-account" constant.
- resourceType: string;
- // Name of the resource. In case of "bank-account", that
- // is the name under which the bank account was imported
- // from the bank.
- resourceId: string;
- // Task name, equals 'taskId'
- taskName: string;
- // Values allowed are "fetch" or "submit".
- taskType: string;
- // FIXME: describe.
- taskCronSpec: string;
- // Only meaningful for "fetch" types.
- taskParams: FetchParams;
- // Timestamp in seconds when the next iteration will run.
- nextScheduledExecutionSec: number;
- // Timestamp in seconds when the previous iteration ran.
- prevScheduledExecutionSec: number;
-}
-
-export interface NexusNewTransactionsInfo {
- // How many transactions are new to Nexus.
- newTransactions: number;
- // How many transactions got downloaded by the request.
- // Note that a transaction can be downloaded multiple
- // times but only counts as new once.
- downloadedTransactions: number;
-}
-
-
-export interface NexusUserResponse {
- // User name
- username: string;
-
- // Is this a super user?
- superuser: boolean;
-}
-
-export interface NexusTaskShortInfo {
- cronspec: string;
- type: "fetch" | "submit";
- params: FetchParams;
-}
-
-export interface NexusTaskCollection {
- // This field can contain *multiple* objects of the type sampled below.
- schedule: {
- [taskName: string]: NexusTaskShortInfo;
- };
-}
-
-export interface NexusFacadeListResponse {
- facades: FacadeShowInfo[];
-}
-
-export interface LibeufinSandboxAdminBankAccountBalance {
- // Balance in the $currency:$amount format.
- balance: AmountString;
- // IBAN of the bank account identified by $accountLabel
- iban: string;
- // BIC of the bank account identified by $accountLabel
- bic: string;
- // Mentions $accountLabel
- label: string;
-}
-
-export interface LibeufinPermission {
- subjectType: string;
- subjectId: string;
- resourceType: string;
- resourceId: string;
- permissionName: string;
-}
-
-export interface NexusGetPermissionsResponse {
- permissions: LibeufinPermission[];
-}
-
-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 registerBankCustomer(
- username: string,
- password: string,
- ): Promise<void> {
- const stdout = await sh(
- this.globalTestState,
- "libeufin-cli-registercustomer",
- "libeufin-cli sandbox demobank register --name='Test Customer'",
- {
- ...process.env,
- LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl + "/demobanks/default",
- LIBEUFIN_SANDBOX_USERNAME: username,
- LIBEUFIN_SANDBOX_PASSWORD: password,
- },
- );
- console.log(stdout);
- }
-
- 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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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.nexusUser.username,
- LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.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 bank 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.createDemobankAccount(
- sb.ebicsBankAccount.label,
- "password-unused",
- { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/access-api/" },
- );
- await LibeufinSandboxApi.createDemobankEbicsSubscriber(
- sb.ebicsBankAccount.subscriber,
- sb.ebicsBankAccount.label,
- { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/" },
- );
- }
- 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++) {
- //FIXME: last line won't compile with the current definition of the type
- //@ts-ignore
- let batches = transactions[i]["camtData"]["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-harness/src/index.ts b/packages/taler-harness/src/index.ts
index f5d4fd2c2..82d8e4326 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -20,7 +20,7 @@
import {
addPaytoQueryParams,
Amounts,
- BankAccessApiClient,
+ TalerCorebankApiClient,
Configuration,
decodeCrock,
j2s,
@@ -236,7 +236,7 @@ deploymentCli
console.log(tipReserveResp);
- const bankAccessApiClient = new BankAccessApiClient(
+ const bankAccessApiClient = new TalerCorebankApiClient(
args.tipTopup.bankAccessUrl,
{
auth: {
@@ -312,8 +312,8 @@ deploymentCli
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
await topupReserveWithDemobank({
amount: "KUDOS:10",
- bankAccessApiBaseUrl:
- "https://bank.demo.taler.net/demobanks/default/access-api/",
+ corebankApiBaseUrl:
+ "https://bank.demo.taler.net/",
exchangeInfo,
http,
reservePub: reserveKeyPair.pub,
@@ -341,8 +341,8 @@ deploymentCli
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
await topupReserveWithDemobank({
amount: "TESTKUDOS:10",
- bankAccessApiBaseUrl:
- "https://bank.test.taler.net/demobanks/default/access-api/",
+ corebankApiBaseUrl:
+ "https://bank.test.taler.net/",
exchangeInfo,
http,
reservePub: reserveKeyPair.pub,
@@ -371,7 +371,7 @@ deploymentCli
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
await topupReserveWithDemobank({
amount: "TESTKUDOS:10",
- bankAccessApiBaseUrl: "http://localhost:8082/taler-bank-access/",
+ corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
exchangeInfo,
http,
reservePub: reserveKeyPair.pub,
diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts
index 7f936a479..5653e22e2 100644
--- a/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts
+++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts
@@ -27,7 +27,7 @@ import {
withdrawViaBankV2,
} from "../harness/helpers.js";
import {
- BankAccessApiClient,
+ TalerCorebankApiClient,
MerchantApiClient,
WireGatewayApiClient,
} from "@gnu-taler/taler-util";
@@ -179,7 +179,7 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) {
// Pay with coin from tipping
{
- const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl);
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
const mbu = await bankClient.createRandomBankUser();
const tipReserveResp = await merchantClient.createTippingReserve({
exchange_url: exchange.baseUrl,
diff --git a/packages/taler-harness/src/integrationtests/test-bank-api.ts b/packages/taler-harness/src/integrationtests/test-bank-api.ts
index a13ff63c7..9c5b06397 100644
--- a/packages/taler-harness/src/integrationtests/test-bank-api.ts
+++ b/packages/taler-harness/src/integrationtests/test-bank-api.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import {
- BankAccessApiClient,
+ TalerCorebankApiClient,
CreditDebitIndicator,
WireGatewayApiClient,
createEddsaKeyPair,
@@ -30,7 +30,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- getPayto,
+ generateRandomPayto,
setupDb,
} from "../harness/harness.js";
@@ -88,18 +88,18 @@ export async function runBankApiTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
- const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl);
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
const bankUser = await bankClient.registerAccount("user1", "pw1");
diff --git a/packages/taler-harness/src/integrationtests/test-deposit.ts b/packages/taler-harness/src/integrationtests/test-deposit.ts
index 7e1bb2a5c..d4bfa3da5 100644
--- a/packages/taler-harness/src/integrationtests/test-deposit.ts
+++ b/packages/taler-harness/src/integrationtests/test-deposit.ts
@@ -23,7 +23,7 @@ import {
TransactionMinorState,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, getPayto } from "../harness/harness.js";
+import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
import {
createSimpleTestkudosEnvironmentV2,
withdrawViaBankV2,
@@ -75,7 +75,7 @@ export async function runDepositTest(t: GlobalTestState) {
WalletApiOperation.CreateDepositGroup,
{
amount: "TESTKUDOS:10",
- depositPaytoUri: getPayto("foo"),
+ depositPaytoUri: generateRandomPayto("foo"),
transactionId: depositTxId,
},
);
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts b/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts
index 96255f5b5..8ad7daa63 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts
@@ -66,7 +66,7 @@ export async function runExchangeDepositTest(t: GlobalTestState) {
await topupReserveWithDemobank({
http,
amount: "TESTKUDOS:10",
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeInfo,
reservePub: reserveKeyPair.pub,
});
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-management.ts b/packages/taler-harness/src/integrationtests/test-exchange-management.ts
index 9338a8988..a7908264d 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-management.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-management.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import {
- BankAccessApiClient,
+ TalerCorebankApiClient,
ExchangesListResponse,
TalerErrorCode,
URL,
@@ -36,7 +36,7 @@ import {
GlobalTestState,
MerchantService,
WalletCli,
- getPayto,
+ generateRandomPayto,
setupDb,
} from "../harness/harness.js";
@@ -105,13 +105,13 @@ export async function runExchangeManagementTest(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
@@ -263,7 +263,7 @@ export async function runExchangeManagementTest(
// Create withdrawal operation
- const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl);
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
const user = await bankClient.createRandomBankUser();
const wop = await bankClient.createWithdrawalOperation(
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
index 5a1d02692..33a09ed16 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
@@ -79,7 +79,7 @@ export async function runExchangePurseTest(t: GlobalTestState) {
amount: "TESTKUDOS:10",
http,
reservePub: reserveKeyPair.pub,
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeInfo,
});
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
index 2ef7683b3..efa21e1a0 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
@@ -35,7 +35,7 @@ import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
ExchangeService,
- getPayto,
+ generateRandomPayto,
GlobalTestState,
MerchantService,
setupDb,
@@ -151,13 +151,13 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-harness/src/integrationtests/test-fee-regression.ts b/packages/taler-harness/src/integrationtests/test-fee-regression.ts
index 2d84b3a7c..f164606c4 100644
--- a/packages/taler-harness/src/integrationtests/test-fee-regression.ts
+++ b/packages/taler-harness/src/integrationtests/test-fee-regression.ts
@@ -23,7 +23,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- getPayto,
+ generateRandomPayto,
setupDb,
} from "../harness/harness.js";
import {
@@ -142,7 +142,7 @@ export async function createMyTestkudosEnvironment(
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-harness/src/integrationtests/test-forced-selection.ts b/packages/taler-harness/src/integrationtests/test-forced-selection.ts
index 3425dadf1..917ad2025 100644
--- a/packages/taler-harness/src/integrationtests/test-forced-selection.ts
+++ b/packages/taler-harness/src/integrationtests/test-forced-selection.ts
@@ -38,7 +38,7 @@ export async function runForcedSelectionTest(t: GlobalTestState) {
await walletClient.call(WalletApiOperation.WithdrawTestBalance, {
exchangeBaseUrl: exchange.baseUrl,
amount: "TESTKUDOS:10",
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
forcedDenomSel: {
denoms: [
{
diff --git a/packages/taler-harness/src/integrationtests/test-kyc.ts b/packages/taler-harness/src/integrationtests/test-kyc.ts
index 1f7358b66..319e8828f 100644
--- a/packages/taler-harness/src/integrationtests/test-kyc.ts
+++ b/packages/taler-harness/src/integrationtests/test-kyc.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import {
- BankAccessApiClient,
+ TalerCorebankApiClient,
Duration,
j2s,
Logger,
@@ -34,7 +34,7 @@ import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
ExchangeService,
- getPayto,
+ generateRandomPayto,
GlobalTestState,
MerchantService,
setupDb,
@@ -162,7 +162,7 @@ export async function createKycTestkudosEnvironment(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -171,7 +171,7 @@ export async function createKycTestkudosEnvironment(
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
@@ -302,7 +302,7 @@ export async function runKycTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet.
- const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl);
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
const amount = "TESTKUDOS:20";
const user = await bankClient.createRandomBankUser();
@@ -331,7 +331,9 @@ export async function runKycTest(t: GlobalTestState) {
// Confirm it
- await bankClient.confirmWithdrawalOperation(user.username, wop);
+ await bankClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
const kycNotificationCond = walletClient.waitForNotificationCond((x) => {
if (
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts
deleted file mode 100644
index e5e3dfe64..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts
+++ /dev/null
@@ -1,108 +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,
- 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.createDemobankAccount(
- "mock",
- "password-unused",
- { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" },
- "DE71500105179674997361",
- );
- await LibeufinSandboxApi.createDemobankEbicsSubscriber(
- {
- hostID: "mock",
- partnerID: "mock",
- userID: "mock",
- },
- "mock",
- { baseUrl: sandbox.baseUrl + "/demobanks/default/" },
- );
- 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: "EUR:1",
- subject: "mock subject",
- },
- );
- await LibeufinNexusApi.fetchTransactions(nexus, "local-mock");
- let transactions = await LibeufinNexusApi.getAccountTransactions(
- nexus,
- "local-mock",
- );
- let el = findNexusPayment("mock subject", transactions);
- t.assertTrue(el instanceof Object);
-}
-
-runLibeufinApiBankaccountTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts
deleted file mode 100644
index 243500dc9..000000000
--- a/packages/taler-harness/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.bankConnections.length == 1);
-
- await LibeufinNexusApi.deleteBankConnection(nexus, {
- bankConnectionId: "bankconnection-api-test-connection",
- });
- connections = await LibeufinNexusApi.getAllConnections(nexus);
- t.assertTrue(connections.bankConnections.length == 0);
-}
-runLibeufinApiBankconnectionTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts
deleted file mode 100644
index 27cc81588..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts
+++ /dev/null
@@ -1,68 +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 { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
-import {
- launchLibeufinServices,
- NexusUserBundle,
- SandboxUserBundle,
-} from "../harness/libeufin.js";
-import {
- createPlatformHttpLib,
- makeBasicAuthHeader,
-} from "@gnu-taler/taler-util/http";
-
-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 harnessHttpLib.fetch(url.href, {
- method: "POST",
- body: {
- name: "malformed-facade",
- type: "taler-wire-gateway",
- config: {}, // malformation here.
- },
- headers: {
- Authorization: makeBasicAuthHeader("admin", "test"),
- },
- });
- t.assertTrue(resp.status == 400);
-}
-
-runLibeufinApiFacadeBadRequestTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts
deleted file mode 100644
index a819dd481..000000000
--- a/packages/taler-harness/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.facades[0].name == user01nexus.twgReq["name"]);
-
- const twgBaseUrl: string = resp.facades[0]["baseUrl"];
- t.assertTrue(typeof twgBaseUrl === "string");
- t.assertTrue(twgBaseUrl.startsWith("http://"));
- t.assertTrue(twgBaseUrl.endsWith("/"));
-
- // delete it.
- await LibeufinNexusApi.deleteFacade(
- libeufinServices.libeufinNexus,
- user01nexus.twgReq["name"],
- );
- resp = await LibeufinNexusApi.getAllFacades(libeufinServices.libeufinNexus);
- t.assertTrue(!resp.hasOwnProperty("facades"));
-}
-
-runLibeufinApiFacadeTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts
deleted file mode 100644
index 56443c20a..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts
+++ /dev/null
@@ -1,65 +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["permissions"].pop();
- t.assertTrue(!!element);
- 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["permissions"].length == 0);
-}
-
-runLibeufinApiPermissionsTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts
deleted file mode 100644
index 22b411dc2..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts
+++ /dev/null
@@ -1,76 +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.createDemobankAccount(
- "mock-account-0",
- "password-unused",
- { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" },
- );
- await LibeufinSandboxApi.createDemobankAccount(
- "mock-account-1",
- "password-unused",
- { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" },
- );
- 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.experimental = true;
-runLibeufinApiSandboxCamtTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts
deleted file mode 100644
index 6cfc55aa6..000000000
--- a/packages/taler-harness/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.createDemobankAccount(
- "mock-account",
- "password-unused",
- { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" },
- "DE71500105179674997361",
- );
- await LibeufinSandboxApi.simulateIncomingTransaction(
- sandbox,
- "mock-account",
- {
- debtorIban: "DE84500105176881385584",
- debtorBic: "BELADEBEXXX",
- debtorName: "mock2",
- subject: "mock subject",
- amount: "EUR:1",
- },
- );
- await LibeufinSandboxApi.simulateIncomingTransaction(
- sandbox,
- "mock-account",
- {
- debtorIban: "DE84500105176881385584",
- debtorBic: "BELADEBEXXX",
- debtorName: "mock2",
- subject: "mock subject 2",
- amount: "EUR:1.1",
- },
- );
- let ret = await LibeufinSandboxApi.getAccountInfoWithBalance(
- sandbox,
- "mock-account",
- );
- t.assertAmountEquals(ret.balance, "EUR:2.1");
-}
-runLibeufinApiSandboxTransactionsTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts
deleted file mode 100644
index 15ed2ab78..000000000
--- a/packages/taler-harness/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.getTask(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- t.assertTrue(resp.taskName == "test-task");
- await LibeufinNexusApi.deleteTask(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- try {
- await LibeufinNexusApi.getTask(
- 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.getTask(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- t.assertTrue(resp.taskName == "test-task");
- await LibeufinNexusApi.deleteTask(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- try {
- await LibeufinNexusApi.getTask(
- nexus,
- user01nexus.localAccountName,
- "test-task",
- );
- } catch (err: any) {
- t.assertTrue(err.response.status == 404);
- }
-}
-runLibeufinApiSchedulingTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts
deleted file mode 100644
index 662b22bbe..000000000
--- a/packages/taler-harness/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);
- t.assertTrue(resp["username"] == "one" && !resp["superuser"]);
-}
-
-runLibeufinApiUsersTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-bad-gateway.ts b/packages/taler-harness/src/integrationtests/test-libeufin-bad-gateway.ts
deleted file mode 100644
index 1187d923b..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-bad-gateway.ts
+++ /dev/null
@@ -1,75 +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-harness/src/integrationtests/test-libeufin-bank.ts b/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts
new file mode 100644
index 000000000..c8c668bed
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts
@@ -0,0 +1,222 @@
+/*
+ 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,
+ CreditDebitIndicator,
+ WireGatewayApiClient,
+ createEddsaKeyPair,
+ encodeCrock,
+ Logger,
+ j2s,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ ExchangeService,
+ GlobalTestState,
+ LibeufinBankService,
+ MerchantService,
+ generateRandomPayto,
+ generateRandomTestIban,
+ setupDb,
+} from "../harness/harness.js";
+import { createWalletDaemonWithClient } from "../harness/helpers.js";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+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);
+
+ // register exchange bank account
+ await bankClient.registerAccountExtended({
+ name: "Exchange",
+ password: exchangeBankPw,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ internal_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();
+
+ const wireGatewayApiClient = new WireGatewayApiClient(wireGatewayApiBaseUrl, {
+ auth: {
+ username: exchangeBankUsername,
+ password: exchangeBankPw,
+ },
+ });
+
+ await wireGatewayApiClient.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-harness/src/integrationtests/test-libeufin-basic.ts b/packages/taler-harness/src/integrationtests/test-libeufin-basic.ts
deleted file mode 100644
index d87278197..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-basic.ts
+++ /dev/null
@@ -1,317 +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,
- MerchantContractTerms,
-} from "@gnu-taler/taler-util";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
-import {
- DbInfo,
- ExchangeService,
- GlobalTestState,
- HarnessExchangeBankAccount,
- MerchantService,
- WalletClient,
- setupDb,
-} from "../harness/harness.js";
-import {
- createWalletDaemonWithClient,
- makeTestPaymentV2,
-} 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;
- walletClient: WalletClient;
- 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: db.connStr,
- });
-
- await libeufinSandbox.start();
- await libeufinSandbox.pingUntilAvailable();
-
- const libeufinNexus = await LibeufinNexusService.create(t, {
- httpPort: 5011,
- databaseJdbcUri: db.connStr,
- });
-
- await libeufinNexus.start();
- await libeufinNexus.pingUntilAvailable();
-
- await LibeufinSandboxApi.createEbicsHost(libeufinSandbox, "host01");
- // Subscriber and bank Account for the exchange
- await LibeufinSandboxApi.createDemobankAccount(
- "exchangeacct",
- "password-unused",
- { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/access-api/" },
- exchangeIban,
- );
- await LibeufinSandboxApi.createDemobankEbicsSubscriber(
- {
- hostID: "host01",
- partnerID: "partner01",
- userID: "user01",
- },
- "exchangeacct",
- { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/" },
- );
-
- await LibeufinSandboxApi.createDemobankAccount(
- "merchantacct",
- "password-unused",
- { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/access-api/" },
- merchantIban,
- );
- await LibeufinSandboxApi.createDemobankEbicsSubscriber(
- {
- hostID: "host01",
- partnerID: "partner02",
- userID: "user02",
- },
- "merchantacct",
- { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/" },
- );
-
- 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.addInstanceWithWireAccount({
- id: "default",
- name: "Default Instance",
- paytoUris: [`payto://iban/${merchantIban}?receiver-name=Merchant`],
- defaultWireTransferDelay: Duration.toTalerProtocolDuration(
- Duration.getZero(),
- ),
- });
-
- console.log("setup done!");
-
- const { walletClient } = await createWalletDaemonWithClient(t, {
- name: "default",
- });
-
- return {
- commonDb: db,
- exchange,
- merchant,
- walletClient,
- exchangeBankAccount,
- libeufinNexus,
- libeufinSandbox,
- };
-}
-
-/**
- * Run basic test with LibEuFin.
- */
-export async function runLibeufinBasicTest(t: GlobalTestState) {
- // Set up test environment
-
- const { walletClient, exchange, merchant, libeufinSandbox, libeufinNexus } =
- await createLibeufinTestEnvironment(t);
-
- await walletClient.call(WalletApiOperation.AddExchange, {
- exchangeBaseUrl: exchange.baseUrl,
- });
-
- const wr = await walletClient.call(
- WalletApiOperation.AcceptManualWithdrawal,
- {
- exchangeBaseUrl: exchange.baseUrl,
- amount: "EUR:15",
- },
- );
-
- const reservePub: string = wr.reservePub;
-
- await LibeufinSandboxApi.simulateIncomingTransaction(
- libeufinSandbox,
- "exchangeacct",
- {
- amount: "EUR:15.00",
- debtorBic: customerBic,
- debtorIban: customerIban,
- debtorName: "Jane Customer",
- subject: `Taler Top-up ${reservePub}`,
- },
- );
-
- await LibeufinNexusApi.fetchTransactions(libeufinNexus, "myacct");
-
- await exchange.runWirewatchOnce();
-
- await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
-
- const bal = await walletClient.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.toProtocolTimestamp(
- AbsoluteTime.now(),
- ),
- };
-
- await makeTestPaymentV2(t, { walletClient, 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-harness/src/integrationtests/test-libeufin-c5x.ts b/packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts
deleted file mode 100644
index 5097bc4d3..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts
+++ /dev/null
@@ -1,147 +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["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",
- );
-
- let expectOne = await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "report", // C52
- );
- t.assertTrue(expectOne.newTransactions == 1);
- t.assertTrue(expectOne.downloadedTransactions == 1);
-
- /* Expect zero payments being downloaded because the
- * previous request consumed already the one pending
- * payment.
- */
- let expectZero = await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "report", // C52
- );
- t.assertTrue(expectZero.newTransactions == 0);
- t.assertTrue(expectZero.downloadedTransactions == 0);
-
- /**
- * A statement should still account zero payments because
- * so far the payment made before is still pending.
- */
- expectZero = await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "statement", // C53
- );
- t.assertTrue(expectZero.newTransactions == 0);
- t.assertTrue(expectZero.downloadedTransactions == 0);
-
- /**
- * Ticking now. That books any pending transaction.
- */
- await libeufinServices.libeufinSandbox.c53tick();
-
- /**
- * A statement is now expected to download the transaction,
- * although that got already ingested along the report
- * earlier. Thus the transaction counts as downloaded but
- * not as new.
- */
- expectOne = await LibeufinNexusApi.fetchTransactions(
- libeufinServices.libeufinNexus,
- user01nexus.localAccountName,
- "all", // range
- "statement", // C53
- );
- t.assertTrue(expectOne.downloadedTransactions == 1);
- t.assertTrue(expectOne.newTransactions == 0);
-}
-runLibeufinC5xTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts b/packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts
deleted file mode 100644
index 0efd55f44..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts
+++ /dev/null
@@ -1,179 +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["facades"][0]["name"] == user01nexus.anastasisReq["name"],
- );
-const anastasisBaseUrl: string = resp["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: "EUR: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: "EUR: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: "EUR: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-harness/src/integrationtests/test-libeufin-keyrotation.ts b/packages/taler-harness/src/integrationtests/test-libeufin-keyrotation.ts
deleted file mode 100644
index a2c21d5d8..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-keyrotation.ts
+++ /dev/null
@@ -1,82 +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);
- // FIXME: uncomment and adapt the following command after #6723 is fixed.
- // t.assertTrue(e.response.data.code == 9000);
- }
-}
-runLibeufinKeyrotationTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts b/packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts
deleted file mode 100644
index 868f93759..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts
+++ /dev/null
@@ -1,117 +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");
-
- // Booking the first two transactions.
- await libeufinServices.libeufinSandbox.c53tick();
-
- // user 01 gives 30
- await libeufinServices.libeufinSandbox.makeTransaction(
- user01sandbox.ebicsBankAccount.label,
- user02sandbox.ebicsBankAccount.label,
- "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.experimental = true;
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-refund-multiple-users.ts b/packages/taler-harness/src/integrationtests/test-libeufin-refund-multiple-users.ts
deleted file mode 100644
index 245f34331..000000000
--- a/packages/taler-harness/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-harness/src/integrationtests/test-libeufin-refund.ts b/packages/taler-harness/src/integrationtests/test-libeufin-refund.ts
deleted file mode 100644
index d37363bab..000000000
--- a/packages/taler-harness/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["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-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts b/packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts
deleted file mode 100644
index be467e2f1..000000000
--- a/packages/taler-harness/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.createDemobankAccount(
- "mock-account",
- "password-unused",
- { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" },
- "DE71500105179674997361",
- );
- await LibeufinSandboxApi.createDemobankAccount(
- "mock-account-2",
- "password-unused",
- { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" },
- "DE71500105179674997364",
- );
-
- 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.balance);
- t.assertTrue(ret.balance == "EUR:1.89");
-}
-runLibeufinSandboxWireTransferCliTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-tutorial.ts b/packages/taler-harness/src/integrationtests/test-libeufin-tutorial.ts
deleted file mode 100644
index 496b65ee3..000000000
--- a/packages/taler-harness/src/integrationtests/test-libeufin-tutorial.ts
+++ /dev/null
@@ -1,130 +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`,
- nexusUser: 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-harness/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
index 2f79041d6..35e3267b1 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
@@ -33,7 +33,7 @@ import {
import {
BankService,
ExchangeService,
- getPayto,
+ generateRandomPayto,
GlobalTestState,
harnessHttpLib,
MerchantService,
@@ -112,13 +112,13 @@ export async function createConfusedMerchantTestkudosEnvironment(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
index ff567d33d..4508b9976 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
@@ -22,7 +22,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- getPayto,
+ generateRandomPayto,
harnessHttpLib,
setupDb,
} from "../harness/harness.js";
@@ -78,7 +78,7 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
@@ -88,7 +88,7 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "myinst",
name: "Second Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts
index 071288b0f..a037a01c5 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts
@@ -22,7 +22,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- getPayto,
+ generateRandomPayto,
harnessHttpLib,
setupDb,
} from "../harness/harness.js";
@@ -74,7 +74,7 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
name: "My Default Instance",
accounts: [
{
- payto_uri: getPayto("bar"),
+ payto_uri: generateRandomPayto("bar"),
},
],
auth: {
@@ -97,7 +97,7 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
name: "My Second Instance",
accounts: [
{
- payto_uri: getPayto("bar"),
+ payto_uri: generateRandomPayto("bar"),
},
],
auth: {
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
index 27de8a0a0..a77e9ca51 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
@@ -23,7 +23,7 @@ import {
GlobalTestState,
MerchantService,
setupDb,
- getPayto,
+ generateRandomPayto,
harnessHttpLib,
} from "../harness/harness.js";
@@ -78,7 +78,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
@@ -88,7 +88,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
@@ -98,7 +98,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "myinst",
name: "Second Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
auth: {
method: "external",
},
diff --git a/packages/taler-harness/src/integrationtests/test-payment-fault.ts b/packages/taler-harness/src/integrationtests/test-payment-fault.ts
index e57427fac..af6751ef4 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-fault.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-fault.ts
@@ -22,7 +22,7 @@
* Imports.
*/
import {
- BankAccessApiClient,
+ TalerCorebankApiClient,
CoreApiResponse,
MerchantApiClient,
} from "@gnu-taler/taler-util";
@@ -39,7 +39,7 @@ import {
GlobalTestState,
MerchantService,
WalletCli,
- getPayto,
+ generateRandomPayto,
setupDb,
} from "../harness/harness.js";
@@ -116,7 +116,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
@@ -127,7 +127,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
// Create withdrawal operation
- const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl);
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
const user = await bankClient.createRandomBankUser();
const wop = await bankClient.createWithdrawalOperation(
@@ -153,7 +153,9 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
// Confirm it
- await bankClient.confirmWithdrawalOperation(user.username, wop);
+ await bankClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
await wallet.runUntilDone();
diff --git a/packages/taler-harness/src/integrationtests/test-payment-multiple.ts b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts
index 4ef5e3bff..0caa3c3e7 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-multiple.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts
@@ -25,7 +25,7 @@ import {
ExchangeService,
GlobalTestState,
MerchantService,
- getPayto,
+ generateRandomPayto,
setupDb,
} from "../harness/harness.js";
import {
@@ -87,13 +87,13 @@ async function setupTest(t: GlobalTestState): Promise<{
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
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
index 25c000808..6d9f44fb5 100644
--- a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
+++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
@@ -73,7 +73,7 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
await withdrawRes.withdrawalFinishedCond;
- const purse_expiration = AbsoluteTime.toProtocolTimestamp(
+ const purseExpiration = AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.addDuration(
AbsoluteTime.now(),
Duration.fromSpec({ days: 2 }),
@@ -87,7 +87,7 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
partialContractTerms: {
summary: "Hello World",
amount: "TESTKUDOS:5",
- purse_expiration,
+ purse_expiration: purseExpiration,
},
},
);
diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts
index 0cb6987ad..9ed2d6206 100644
--- a/packages/taler-harness/src/integrationtests/test-revocation.ts
+++ b/packages/taler-harness/src/integrationtests/test-revocation.ts
@@ -27,7 +27,7 @@ import {
setupDb,
BankService,
delayMs,
- getPayto,
+ generateRandomPayto,
WalletClient,
} from "../harness/harness.js";
import {
@@ -125,13 +125,13 @@ async function createTestEnvironment(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
index b94f7757c..449142809 100644
--- a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
+++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
@@ -32,7 +32,7 @@ import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
ExchangeService,
- getPayto,
+ generateRandomPayto,
GlobalTestState,
MerchantService,
setupDb,
@@ -97,13 +97,13 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
- paytoUris: [getPayto("minst1")],
+ paytoUris: [generateRandomPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-harness/src/integrationtests/test-tipping.ts b/packages/taler-harness/src/integrationtests/test-tipping.ts
index 4140311ab..12cdbae53 100644
--- a/packages/taler-harness/src/integrationtests/test-tipping.ts
+++ b/packages/taler-harness/src/integrationtests/test-tipping.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import {
- BankAccessApiClient,
+ TalerCorebankApiClient,
MerchantApiClient,
TransactionMajorState,
WireGatewayApiClient,
@@ -38,8 +38,8 @@ export async function runTippingTest(t: GlobalTestState) {
const { walletClient, bank, exchange, merchant, exchangeBankAccount } =
await createSimpleTestkudosEnvironmentV2(t);
- const bankAccessApiClient = new BankAccessApiClient(
- bank.bankAccessApiBaseUrl,
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
);
const mbu = await bankAccessApiClient.createRandomBankUser();
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
index 153ae93d8..5e6539654 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
@@ -72,7 +72,7 @@ export async function runWalletDblessTest(t: GlobalTestState) {
amount: "TESTKUDOS:10",
http,
reservePub: reserveKeyPair.pub,
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeInfo,
});
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..ff6ed9959
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-gendb.ts
@@ -0,0 +1,110 @@
+/*
+ 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,
+ 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",
+ 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-notifications.ts b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
index 9a0eb77ae..c87a9a264 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import {
- BankAccessApiClient,
+ TalerCorebankApiClient,
Duration,
NotificationType,
TransactionMajorState,
@@ -32,7 +32,7 @@ import {
MerchantService,
WalletClient,
WalletService,
- getRandomIban,
+ generateRandomTestIban,
setupDb,
} from "../harness/harness.js";
@@ -94,7 +94,7 @@ export async function runWalletNotificationsTest(t: GlobalTestState) {
id: "default",
name: "Default Instance",
paytoUris: [
- `payto://iban/SANDBOXX/${getRandomIban(label)}?receiver-name=${label}`,
+ `payto://iban/SANDBOXX/${generateRandomTestIban(label)}?receiver-name=${label}`,
],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
@@ -121,8 +121,8 @@ export async function runWalletNotificationsTest(t: GlobalTestState) {
skipDefaults: true,
});
- const bankAccessApiClient = new BankAccessApiClient(
- bank.bankAccessApiBaseUrl,
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
);
const user = await bankAccessApiClient.createRandomBankUser();
bankAccessApiClient.setAuth(user);
@@ -161,7 +161,9 @@ export async function runWalletNotificationsTest(t: GlobalTestState) {
// Confirm it
- await bankAccessApiClient.confirmWithdrawalOperation(user.username, wop);
+ await bankAccessApiClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
await withdrawalFinishedReceivedPromise;
}
diff --git a/packages/taler-harness/src/integrationtests/test-wallettesting.ts b/packages/taler-harness/src/integrationtests/test-wallettesting.ts
index 4fa870f1c..e5191aa5b 100644
--- a/packages/taler-harness/src/integrationtests/test-wallettesting.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallettesting.ts
@@ -32,7 +32,7 @@ import {
MerchantService,
setupDb,
WalletCli,
- getPayto,
+ generateRandomPayto,
} from "../harness/harness.js";
import { SimpleTestEnvironment } from "../harness/helpers.js";
@@ -94,7 +94,7 @@ export async function createMyEnvironment(
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
- paytoUris: [getPayto("merchant-default")],
+ paytoUris: [generateRandomPayto("merchant-default")],
});
console.log("setup done!");
@@ -120,7 +120,7 @@ export async function runWallettestingTest(t: GlobalTestState) {
await wallet.client.call(WalletApiOperation.RunIntegrationTest, {
amountToSpend: "TESTKUDOS:5",
amountToWithdraw: "TESTKUDOS:10",
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeBaseUrl: exchange.baseUrl,
merchantAuthToken: merchantAuthToken,
merchantBaseUrl: merchant.makeInstanceBaseUrl(),
@@ -143,7 +143,7 @@ export async function runWallettestingTest(t: GlobalTestState) {
await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
amount: "TESTKUDOS:10",
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeBaseUrl: exchange.baseUrl,
});
@@ -168,7 +168,7 @@ export async function runWallettestingTest(t: GlobalTestState) {
await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
amount: "TESTKUDOS:10",
- bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
exchangeBaseUrl: exchange.baseUrl,
});
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts
index aa5e2b770..f9f2df0fc 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts
@@ -17,7 +17,7 @@
/**
* Imports.
*/
-import { BankAccessApiClient, TalerErrorCode } from "@gnu-taler/taler-util";
+import { TalerCorebankApiClient, TalerErrorCode } 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";
@@ -33,8 +33,8 @@ export async function runWithdrawalAbortBankTest(t: GlobalTestState) {
// Create a withdrawal operation
- const bankAccessApiClient = new BankAccessApiClient(
- bank.bankAccessApiBaseUrl,
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
);
const user = await bankAccessApiClient.createRandomBankUser();
bankAccessApiClient.setAuth(user);
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
index 232b6d7c2..76dec50d3 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import {
- BankAccessApiClient,
+ TalerCorebankApiClient,
j2s,
NotificationType,
TransactionMajorState,
@@ -41,12 +41,12 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
// Create a withdrawal operation
- const bankAccessApiClient = new BankAccessApiClient(
- bank.bankAccessApiBaseUrl,
+ const corebankApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
);
- const user = await bankAccessApiClient.createRandomBankUser();
- bankAccessApiClient.setAuth(user);
- const wop = await bankAccessApiClient.createWithdrawalOperation(
+ const user = await corebankApiClient.createRandomBankUser();
+ corebankApiClient.setAuth(user);
+ const wop = await corebankApiClient.createWithdrawalOperation(
user.username,
"TESTKUDOS:10",
);
@@ -129,7 +129,9 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
// Confirm it
- await bankAccessApiClient.confirmWithdrawalOperation(user.username, wop);
+ await corebankApiClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
await withdrawalBankConfirmedCond;
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts
index ec6e54e6c..e3057451e 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts
@@ -54,7 +54,10 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) {
exchange.addBankAccount("1", {
accountName: "exchange",
accountPassword: "x",
- wireGatewayApiBaseUrl: new URL("/exchange/", bank.baseUrl).href,
+ wireGatewayApiBaseUrl: new URL(
+ "/accounts/exchange/taler-wire-gateway",
+ bank.baseUrl,
+ ).href,
accountPaytoUri: "payto://x-taler-bank/localhost/exchange",
});
@@ -76,10 +79,10 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) {
exchangeBaseUrl: exchange.baseUrl,
});
- await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
- exchange: exchange.baseUrl,
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ exchangeBaseUrl: exchange.baseUrl,
amount: "TESTKUDOS:10",
- bank: bank.baseUrl,
});
await exchange.runWirewatchOnce();
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
index bc2946a18..f702376e1 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts
@@ -17,7 +17,7 @@
/**
* Imports.
*/
-import { BankAccessApiClient, j2s } from "@gnu-taler/taler-util";
+import { TalerCorebankApiClient, j2s } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig } from "../harness/denomStructures.js";
import {
@@ -107,8 +107,8 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) {
const amount = "TESTKUDOS:7.5";
- const bankAccessApiClient = new BankAccessApiClient(
- bank.bankAccessApiBaseUrl,
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
);
const user = await bankAccessApiClient.createRandomBankUser();
bankAccessApiClient.setAuth(user);
@@ -152,7 +152,9 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) {
// Confirm it
- await bankAccessApiClient.confirmWithdrawalOperation(user.username, wop);
+ await bankAccessApiClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
await wallet.runUntilDone();
// Check balance
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts
index 8777b19e2..893d870e5 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts
@@ -99,10 +99,10 @@ export async function runWithdrawalHugeTest(t: GlobalTestState) {
});
// Results in about 1K coins withdrawn
- await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
- exchange: exchange.baseUrl,
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ exchangeBaseUrl: exchange.baseUrl,
amount: "TESTKUDOS:10000",
- bank: bank.baseUrl,
+ corebankApiBaseUrl: bank.baseUrl,
});
await withdrawalFinishedCond;
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts
index 1d98cd46e..fa483aa28 100644
--- a/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts
@@ -19,7 +19,7 @@
*/
import {
AbsoluteTime,
- BankAccessApiClient,
+ TalerCorebankApiClient,
Logger,
WireGatewayApiClient,
j2s,
@@ -41,8 +41,8 @@ export async function runWithdrawalManualTest(t: GlobalTestState) {
// Create a withdrawal operation
- const bankAccessApiClient = new BankAccessApiClient(
- bank.bankAccessApiBaseUrl,
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
);
const user = await bankAccessApiClient.createRandomBankUser();
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 66bd87a59..cf5691fe3 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -43,25 +43,6 @@ import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
import { runFeeRegressionTest } from "./test-fee-regression.js";
import { runForcedSelectionTest } from "./test-forced-selection.js";
import { runKycTest } from "./test-kyc.js";
-import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount.js";
-import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection.js";
-import { runLibeufinApiFacadeBadRequestTest } from "./test-libeufin-api-facade-bad-request.js";
-import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade.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 { runLibeufinRefundMultipleUsersTest } from "./test-libeufin-refund-multiple-users.js";
-import { runLibeufinRefundTest } from "./test-libeufin-refund.js";
-import { runLibeufinSandboxWireTransferCliTest } from "./test-libeufin-sandbox-wire-transfer-cli.js";
-import { runLibeufinTutorialTest } from "./test-libeufin-tutorial.js";
import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion.js";
import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete.js";
import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls.js";
@@ -110,6 +91,8 @@ import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js";
import { runWithdrawalManualTest } from "./test-withdrawal-manual.js";
+import { runWalletGenDbTest } from "./test-wallet-gendb.js";
+import { runLibeufinBankTest } from "./test-libeufin-bank.js";
/**
* Test runner.
@@ -143,25 +126,6 @@ const allTests: TestMainFunction[] = [
runKycTest,
runExchangePurseTest,
runExchangeDepositTest,
- runLibeufinAnastasisFacadeTest,
- runLibeufinApiBankaccountTest,
- runLibeufinApiBankconnectionTest,
- runLibeufinApiFacadeBadRequestTest,
- runLibeufinApiFacadeTest,
- runLibeufinApiPermissionsTest,
- runLibeufinApiSandboxCamtTest,
- runLibeufinApiSandboxTransactionsTest,
- runLibeufinApiSchedulingTest,
- runLibeufinApiUsersTest,
- runLibeufinBadGatewayTest,
- runLibeufinBasicTest,
- runLibeufinC5xTest,
- runLibeufinKeyrotationTest,
- runLibeufinNexusBalanceTest,
- runLibeufinRefundMultipleUsersTest,
- runLibeufinRefundTest,
- runLibeufinSandboxWireTransferCliTest,
- runLibeufinTutorialTest,
runMerchantExchangeConfusionTest,
runMerchantInstancesDeleteTest,
runMerchantInstancesTest,
@@ -209,6 +173,8 @@ const allTests: TestMainFunction[] = [
runTermOfServiceFormatTest,
runStoredBackupsTest,
runPaymentExpiredTest,
+ runWalletGenDbTest,
+ runLibeufinBankTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index 80e99ae0e..7fceb9576 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.3-dev.17",
+ "version": "0.9.3-dev.27",
"description": "Generic helper functionality for GNU Taler",
"type": "module",
"types": "./lib/index.node.d.ts",
@@ -82,4 +82,4 @@
"lib/**/*test.js"
]
}
-}
+} \ No newline at end of file
diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts
index ccbbf79b3..988872ae7 100644
--- a/packages/taler-util/src/MerchantApiClient.ts
+++ b/packages/taler-util/src/MerchantApiClient.ts
@@ -269,7 +269,7 @@ export class MerchantApiClient {
}
async giveTip(req: RewardCreateRequest): Promise<RewardCreateConfirmation> {
- const reqUrl = new URL(`private/tips`, this.baseUrl);
+ const reqUrl = new URL(`private/rewards`, this.baseUrl);
const resp = await this.httpClient.fetch(reqUrl.href, {
method: "POST",
body: req,
diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts
index a8df3679f..04343b8e9 100644
--- a/packages/taler-util/src/amounts.ts
+++ b/packages/taler-util/src/amounts.ts
@@ -345,9 +345,12 @@ export class Amounts {
/**
* 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;
}
diff --git a/packages/taler-util/src/bank-api-client.ts b/packages/taler-util/src/bank-api-client.ts
index cc4123500..a8cd4b0da 100644
--- a/packages/taler-util/src/bank-api-client.ts
+++ b/packages/taler-util/src/bank-api-client.ts
@@ -146,11 +146,91 @@ export class WireGatewayApiClient {
}
}
+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.
+ internal_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;
+}
+
/**
- * This API look like it belongs to harness
- * but it will be nice to have in utils to be used by others
+ * Client for the Taler corebank API.
*/
-export class BankAccessApiClient {
+export class TalerCorebankApiClient {
httpLib: HttpRequestLibrary;
constructor(
@@ -184,7 +264,7 @@ export class BankAccessApiClient {
const resp = await this.httpLib.fetch(url.href, {
headers: this.makeAuthHeader(),
});
- return await resp.json();
+ return readSuccessResponseJsonOrThrow(resp, codecForAny());
}
async getTransactions(username: string): Promise<void> {
@@ -215,24 +295,53 @@ export class BankAccessApiClient {
return await readSuccessResponseJsonOrThrow(resp, codecForAny());
}
- async registerAccount(
- username: string,
- password: string,
- options: {
- iban?: string;
- } = {},
- ): Promise<BankUser> {
- const url = new URL("testing/register", this.baseUrl);
+ 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,
+ });
+
+ 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,
- iban: options?.iban,
+ name: username,
},
});
- let paytoUri = `payto://x-taler-bank/localhost/${username}`;
- if (resp.status !== 200 && resp.status !== 202 && resp.status !== 204) {
+ 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,
@@ -241,31 +350,29 @@ export class BankAccessApiClient {
},
);
}
- 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
- }
+ // 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: paytoUri,
+ accountPaytoUri: acctInfo.payto_uri,
};
}
async createRandomBankUser(): Promise<BankUser> {
const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase();
const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase();
- // FIXME: This is just a temporary workaround, because demobank is running out of short IBANs
- const iban = generateIban("DE", 15);
- return await this.registerAccount(username, password, {
- iban,
- });
+ return await this.registerAccount(username, password);
}
async createWithdrawalOperation(
@@ -288,10 +395,10 @@ export class BankAccessApiClient {
async confirmWithdrawalOperation(
username: string,
- wopi: WithdrawalOperationInfo,
+ wopi: ConfirmWithdrawalArgs,
): Promise<void> {
const url = new URL(
- `accounts/${username}/withdrawals/${wopi.withdrawal_id}/confirm`,
+ `withdrawals/${wopi.withdrawalOperationId}/confirm`,
this.baseUrl,
);
logger.info(`confirming withdrawal operation via ${url.href}`);
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
index 4399dbcf2..07a402413 100644
--- a/packages/taler-util/src/errors.ts
+++ b/packages/taler-util/src/errors.ts
@@ -78,7 +78,7 @@ export interface DetailsMap {
stack?: string;
};
[TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: {
- exchangeProtocolVersion: string;
+ bankProtocolVersion: string;
walletProtocolVersion: string;
};
[TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index 60c4ba838..85870afcd 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -89,12 +89,13 @@ export function buildPayto(
return result;
}
case "iban": {
+ const uppercased = first.toUpperCase()
const result: PaytoUriIBAN = {
isKnown: true,
targetType: "iban",
- iban: first,
+ iban: uppercased,
params: {},
- targetPath: !second ? first : `${second}/${first}`,
+ targetPath: !second ? uppercased : `${second}/${uppercased}`,
};
return result;
}
@@ -200,13 +201,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,
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 8a0608008..ebb0291f5 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -2005,22 +2005,13 @@ export interface BatchDepositSuccess {
// 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_sigs: DepositConfirmationSignature[];
+ exchange_sig: EddsaSignatureString;
}
-export const codecForDepositConfirmationSignature =
- (): Codec<DepositConfirmationSignature> =>
- buildCodecForObject<DepositConfirmationSignature>()
- .property("exchange_sig", codecForString())
- .build("DepositConfirmationSignature");
-
export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> =>
buildCodecForObject<BatchDepositSuccess>()
.property("exchange_pub", codecForString())
- .property(
- "exchange_sigs",
- codecForList(codecForDepositConfirmationSignature()),
- )
+ .property("exchange_sig", codecForString())
.property("exchange_timestamp", codecForTimestamp)
.property("transaction_base_url", codecOptional(codecForString()))
.build("BatchDepositSuccess");
diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts
index e9eb71279..f817d9bcb 100644
--- a/packages/taler-util/src/talerconfig.ts
+++ b/packages/taler-util/src/talerconfig.ts
@@ -143,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 >= 128) {
+ if (recursionDepth >= 128) {
throw Error("recursion in path substitution");
}
let s = x;
@@ -201,7 +201,7 @@ 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.substring(0, l) + r + s.substring(l + 1 + m[0].length);
l = l + r.length;
diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts
index 46ed37637..c677d52ae 100644
--- a/packages/taler-util/src/time.ts
+++ b/packages/taler-util/src/time.ts
@@ -52,6 +52,10 @@ export interface TalerProtocolTimestamp {
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.
@@ -88,7 +92,7 @@ export namespace TalerPreciseTimestamp {
export function fromMilliseconds(ms: number): TalerPreciseTimestamp {
return {
t_s: Math.floor(ms / 1000),
- off_us: Math.floor((ms - Math.floor(ms / 100) * 1000) * 1000),
+ off_us: Math.floor((ms - Math.floor(ms / 1000) * 1000) * 1000),
};
}
}
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index 304183ceb..63db206bd 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -67,7 +67,7 @@ export interface TransactionsRequest {
*/
includeRefreshes?: boolean;
- filterByState?: TransactionStateFilter
+ filterByState?: TransactionStateFilter;
}
export interface TransactionState {
@@ -629,6 +629,17 @@ export interface TransactionRefresh extends TransactionCommon {
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;
+}
+
/**
* Deposit transaction, which effectively sends
* money from this wallet somewhere else.
@@ -662,16 +673,7 @@ export interface TransactionDeposit extends TransactionCommon {
*/
deposited: boolean;
- trackingState: Array<{
- // 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;
- }>;
+ trackingState: Array<DepositTransactionTrackingState>;
}
export interface TransactionByIdRequest {
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index f7bd3d120..4811d674f 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -73,7 +73,13 @@ import {
codecForAbsoluteTime,
codecForTimestamp,
} from "./time.js";
-import { OrderShortInfo, TransactionType } from "./transactions-types.js";
+import {
+ OrderShortInfo,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionState,
+ TransactionType,
+} from "./transactions-types.js";
/**
* Identifier for a transaction in the wallet.
@@ -366,7 +372,7 @@ export const codecForAmountResponse = (): Codec<AmountResponse> =>
.property("rawAmount", codecForAmountString())
.build("AmountResponse");
-export interface Balance {
+export interface WalletBalance {
scopeInfo: ScopeInfo;
available: AmountString;
pendingIncoming: AmountString;
@@ -458,11 +464,11 @@ export type ScopeInfoAuditor = {
export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;
export interface BalancesResponse {
- balances: Balance[];
+ balances: WalletBalance[];
}
-export const codecForBalance = (): Codec<Balance> =>
- buildCodecForObject<Balance>()
+export const codecForBalance = (): Codec<WalletBalance> =>
+ buildCodecForObject<WalletBalance>()
.property("scopeInfo", codecForAny()) // FIXME
.property("available", codecForString())
.property("hasPendingTransactions", codecForBoolean())
@@ -1550,7 +1556,7 @@ export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
export interface IntegrationTestArgs {
exchangeBaseUrl: string;
- bankAccessApiBaseUrl: string;
+ corebankApiBaseUrl: string;
merchantBaseUrl: string;
merchantAuthToken?: string;
amountToWithdraw: string;
@@ -1564,12 +1570,12 @@ export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
.property("merchantAuthToken", codecOptional(codecForString()))
.property("amountToSpend", codecForAmountString())
.property("amountToWithdraw", codecForAmountString())
- .property("bankAccessApiBaseUrl", codecForAmountString())
+ .property("corebankApiBaseUrl", codecForAmountString())
.build("IntegrationTestArgs");
export interface IntegrationTestV2Args {
exchangeBaseUrl: string;
- bankAccessApiBaseUrl: string;
+ corebankApiBaseUrl: string;
merchantBaseUrl: string;
merchantAuthToken?: string;
}
@@ -1579,7 +1585,7 @@ export const codecForIntegrationTestV2Args = (): Codec<IntegrationTestV2Args> =>
.property("exchangeBaseUrl", codecForString())
.property("merchantBaseUrl", codecForString())
.property("merchantAuthToken", codecOptional(codecForString()))
- .property("bankAccessApiBaseUrl", codecForAmountString())
+ .property("corebankApiBaseUrl", codecForAmountString())
.build("IntegrationTestV2Args");
export interface AddExchangeRequest {
@@ -1595,6 +1601,15 @@ export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
.property("masterPub", codecOptional(codecForString()))
.build("AddExchangeRequest");
+export interface UpdateExchangeEntryRequest {
+ exchangeBaseUrl: string;
+}
+
+export const codecForUpdateExchangeEntryRequest = (): Codec<UpdateExchangeEntryRequest> =>
+ buildCodecForObject<UpdateExchangeEntryRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .build("UpdateExchangeEntryRequest");
+
export interface ForceExchangeUpdateRequest {
exchangeBaseUrl: string;
}
@@ -1846,9 +1861,9 @@ export interface CoreApiResponseError {
export interface WithdrawTestBalanceRequest {
amount: string;
/**
- * Bank access API base URL.
+ * Corebank API base URL.
*/
- bankAccessApiBaseUrl: string;
+ corebankApiBaseUrl: string;
exchangeBaseUrl: string;
forcedDenomSel?: ForcedDenomSel;
}
@@ -1921,7 +1936,7 @@ export const codecForWithdrawTestBalance =
.property("amount", codecForString())
.property("exchangeBaseUrl", codecForString())
.property("forcedDenomSel", codecForAny())
- .property("bankAccessApiBaseUrl", codecForString())
+ .property("corebankApiBaseUrl", codecForString())
.build("WithdrawTestBalanceRequest");
export interface SetCoinSuspendedRequest {
@@ -2715,3 +2730,8 @@ export interface WalletContractData {
maxDepositFee: AmountString;
minimumAge?: number;
}
+
+export interface TestingWaitTransactionRequest {
+ transactionId: string;
+ txState: TransactionState;
+}
diff --git a/packages/taler-wallet-cli/Makefile b/packages/taler-wallet-cli/Makefile
index 6d695e9c1..229fb3547 100644
--- a/packages/taler-wallet-cli/Makefile
+++ b/packages/taler-wallet-cli/Makefile
@@ -3,6 +3,7 @@
ifeq ($(TOPLEVEL), yes)
$(info top-level build)
-include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
else
$(info package-level build)
-include ../../.config.mk
@@ -20,23 +21,23 @@ warn-noprefix:
@echo "no prefix configured, did you run ./configure?"
install: warn-noprefix
else
-install_target = $(prefix)/lib/taler-wallet-cli
+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 $(install_target)
- install -d $(prefix)/bin
- install -d $(install_target)/build
- 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-bundled.cjs $(install_target)/node_modules/taler-wallet-cli/dist/
- install ./dist/taler-wallet-cli-bundled.cjs.map $(install_target)/node_modules/taler-wallet-cli/dist/
- install ./bin/taler-wallet-cli.mjs $(install_target)/node_modules/taler-wallet-cli/bin/
- install ../idb-bridge/node_modules/better-sqlite3/build/Release/better_sqlite3.node $(install_target)/build/ \
+ @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 $(install_target)/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs $(prefix)/bin/taler-wallet-cli
+ 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...
pnpm run --filter @gnu-taler/taler-wallet-cli... compile
diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json
index 382951223..571371b87 100644
--- a/packages/taler-wallet-cli/package.json
+++ b/packages/taler-wallet-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-cli",
- "version": "0.9.3-dev.17",
+ "version": "0.9.3-dev.27",
"description": "",
"engines": {
"node": ">=0.18.0"
@@ -41,4 +41,4 @@
"@gnu-taler/taler-wallet-core": "workspace:*",
"tslib": "^2.5.3"
}
-}
+} \ No newline at end of file
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index 3fc86d0b5..f3b205211 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -53,6 +53,7 @@ import {
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { JsonMessage, runRpcServer } from "@gnu-taler/taler-util/twrpc";
import {
+ AccessStats,
createNativeWalletHost,
createNativeWalletHost2,
Wallet,
@@ -237,16 +238,21 @@ export interface WalletContext {
): Promise<T>;
}
+interface CreateWalletResult {
+ wallet: Wallet;
+ getStats: () => AccessStats;
+}
+
async function createLocalWallet(
walletCliArgs: WalletCliArgsType,
notificationHandler?: (n: WalletNotification) => void,
-): Promise<Wallet> {
+): Promise<CreateWalletResult> {
const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath;
const myHttpLib = createPlatformHttpLib({
enableThrottling: walletCliArgs.wallet.noThrottle ? false : true,
requireTls: walletCliArgs.wallet.noHttp,
});
- const wallet = await createNativeWalletHost({
+ const wh = await createNativeWalletHost2({
persistentStoragePath: dbPath !== ":memory:" ? dbPath : undefined,
httpLib: myHttpLib,
notifyHandler: (n) => {
@@ -269,10 +275,10 @@ async function createLocalWallet(
applyVerbose(walletCliArgs.wallet.verbose);
try {
- await wallet.handleCoreApiRequest("initWallet", "native-init", {
+ await wh.wallet.handleCoreApiRequest("initWallet", "native-init", {
skipDefaults: walletCliArgs.wallet.skipDefaults,
});
- return wallet;
+ return { wallet: wh.wallet, getStats: wh.getDbStats };
} catch (e) {
const ed = getErrorDetailFromException(e);
console.error("Operation failed: " + summarizeTalerErrorDetail(ed));
@@ -307,16 +313,20 @@ async function withWallet<T>(
w.close();
return res;
} else {
- const w = await createLocalWallet(walletCliArgs, waiter.notify);
+ const wh = await createLocalWallet(walletCliArgs, waiter.notify);
const ctx: WalletContext = {
- client: w.client,
+ client: wh.wallet.client,
waitForNotificationCond: waiter.waitForNotificationCond,
makeCoreApiRequest(operation, payload) {
- return w.handleCoreApiRequest(operation, "my-req", payload);
+ return wh.wallet.handleCoreApiRequest(operation, "my-req", payload);
},
};
const result = await f(ctx);
- w.stop();
+ wh.wallet.stop();
+ if (process.env.TALER_WALLET_DBSTATS) {
+ console.log("database stats:");
+ console.log(j2s(wh.getStats()));
+ }
return result;
}
}
@@ -330,7 +340,8 @@ async function withLocalWallet<T>(
walletCliArgs: WalletCliArgsType,
f: (w: { client: WalletCoreApiClient; ws: Wallet }) => Promise<T>,
): Promise<T> {
- const w = await createLocalWallet(walletCliArgs);
+ const wh = await createLocalWallet(walletCliArgs);
+ const w = wh.wallet;
const res = await f({ client: w.client, ws: w });
w.stop();
return res;
@@ -1030,8 +1041,7 @@ peerCli
const resp = await wallet.client.call(
WalletApiOperation.ConfirmPeerPullDebit,
{
- peerPullDebitId:
- args.confirmIncomingPayPull.peerPullDebitId,
+ peerPullDebitId: args.confirmIncomingPayPull.peerPullDebitId,
},
);
console.log(JSON.stringify(resp, undefined, 2));
@@ -1046,8 +1056,7 @@ peerCli
const resp = await wallet.client.call(
WalletApiOperation.ConfirmPeerPushCredit,
{
- peerPushCreditId:
- args.confirmIncomingPayPush.peerPushCreditId,
+ peerPushCreditId: args.confirmIncomingPayPush.peerPushCreditId,
},
);
console.log(JSON.stringify(resp, undefined, 2));
@@ -1174,7 +1183,8 @@ advancedCli
})
.action(async (args) => {
logger.info(`serving at ${args.serve.unixPath}`);
- const w = await createLocalWallet(args);
+ const wh = await createLocalWallet(args);
+ const w = wh.wallet;
w.runTaskLoop()
.then((res) => {
logger.warn("task loop exited unexpectedly");
@@ -1270,7 +1280,7 @@ advancedCli
await wallet.client.call(WalletApiOperation.RunIntegrationTest, {
amountToSpend: "TESTKUDOS:1",
amountToWithdraw: "TESTKUDOS:3",
- bankAccessApiBaseUrl: "http://localhost:8082/taler-bank-access/",
+ corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
exchangeBaseUrl: "http://localhost:8081/",
merchantBaseUrl: "http://localhost:8083/",
});
@@ -1281,29 +1291,6 @@ advancedCli
});
advancedCli
- .subcommand("withdrawFakebank", "withdraw-fakebank", {
- help: "Withdraw via a fakebank.",
- })
- .requiredOption("exchange", ["--exchange"], clk.STRING, {
- help: "Base URL of the exchange to use",
- })
- .requiredOption("amount", ["--amount"], clk.STRING, {
- help: "Amount to withdraw (before fees).",
- })
- .requiredOption("bank", ["--bank"], clk.STRING, {
- help: "Base URL of the Taler fakebank service.",
- })
- .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,
- });
- });
- });
-
-advancedCli
.subcommand("genSegwit", "gen-segwit")
.requiredArgument("paytoUri", clk.STRING)
.requiredArgument("reservePub", clk.STRING)
@@ -1520,8 +1507,8 @@ testCli.subcommand("withdrawKudos", "withdraw-kudos").action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
amount: "KUDOS:50",
- bankAccessApiBaseUrl:
- "https://bank.demo.taler.net/demobanks/default/access-api/",
+ corebankApiBaseUrl:
+ "https://bank.demo.taler.net/",
exchangeBaseUrl: "https://exchange.demo.taler.net/",
});
});
diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json
index 668fe4b92..beef99840 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-core",
- "version": "0.9.3-dev.17",
+ "version": "0.9.3-dev.27",
"description": "",
"engines": {
"node": ">=0.18.0"
@@ -84,4 +84,4 @@
"lib/**/*test.*"
]
}
-}
+} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index 35777e714..56392f090 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -87,7 +87,7 @@ import {
WithdrawalPlanchet,
} from "@gnu-taler/taler-util";
// FIXME: Crypto should not use DB Types!
-import { DenominationRecord } from "../db.js";
+import { DenominationRecord, timestampProtocolFromDb } from "../db.js";
import {
CreateRecoupRefreshReqRequest,
CreateRecoupReqRequest,
@@ -962,10 +962,22 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
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))
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 239a6d4a4..46a073156 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -27,8 +27,8 @@ import {
structuredEncapsulate,
} from "@gnu-taler/idb-bridge";
import {
+ AbsoluteTime,
AgeCommitmentProof,
- AmountJson,
AmountString,
Amounts,
AttentionInfo,
@@ -45,23 +45,20 @@ import {
ExchangeAuditor,
ExchangeGlobalFees,
HashCodeString,
- InternationalizedString,
Logger,
- MerchantContractTerms,
- MerchantInfo,
PayCoinSelection,
- PeerContractTerms,
RefreshReason,
TalerErrorDetail,
TalerPreciseTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
+ //TalerProtocolTimestamp,
TransactionIdStr,
UnblindedSignature,
WireInfo,
codecForAny,
} from "@gnu-taler/taler-util";
-import { RetryInfo, TaskIdentifiers } from "./operations/common.js";
+import { DbRetryInfo, TaskIdentifiers } from "./operations/common.js";
import {
DbAccess,
DbReadOnlyTransaction,
@@ -151,6 +148,91 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
*/
export const WALLET_DB_MINOR_VERSION = 1;
+declare const symDbProtocolTimestamp: unique symbol;
+
+declare const symDbPreciseTimestamp: unique symbol;
+
+/**
+ * Timestamp, stored as microseconds.
+ *
+ * Always rounded to a full second.
+ */
+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
@@ -217,7 +299,7 @@ export enum WithdrawalGroupStatus {
* Exchange is doing AML checks.
*/
PendingAml = 0x0100_0006,
- SuspendedAml = 0x0100_0006,
+ SuspendedAml = 0x0110_0006,
/**
* The corresponding withdraw record has been created.
@@ -268,14 +350,14 @@ export interface ReserveBankInfo {
*
* Set to undefined if that hasn't happened yet.
*/
- timestampReserveInfoPosted: TalerPreciseTimestamp | undefined;
+ timestampReserveInfoPosted: DbPreciseTimestamp | undefined;
/**
* Time when the reserve was confirmed by the bank.
*
* Set to undefined if not confirmed yet.
*/
- timestampBankConfirmed: TalerPreciseTimestamp | undefined;
+ timestampBankConfirmed: DbPreciseTimestamp | undefined;
}
/**
@@ -349,22 +431,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
@@ -406,7 +488,7 @@ export interface DenominationRecord {
* Latest list issue date of the "/keys" response
* that includes this denomination.
*/
- listIssueDate: TalerProtocolTimestamp;
+ listIssueDate: DbProtocolTimestamp;
}
export namespace DenominationRecord {
@@ -418,10 +500,10 @@ 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,
+ stampExpireDeposit: timestampProtocolFromDb(d.stampExpireDeposit),
+ stampExpireLegal: timestampProtocolFromDb(d.stampExpireLegal),
+ stampExpireWithdraw: timestampProtocolFromDb(d.stampExpireWithdraw),
+ stampStart: timestampProtocolFromDb(d.stampStart),
value: Amounts.stringify(d.value),
exchangeBaseUrl: d.exchangeBaseUrl,
};
@@ -429,9 +511,9 @@ export namespace DenominationRecord {
}
export interface ExchangeSignkeysRecord {
- stampStart: TalerProtocolTimestamp;
- stampExpire: TalerProtocolTimestamp;
- stampEnd: TalerProtocolTimestamp;
+ stampStart: DbProtocolTimestamp;
+ stampExpire: DbProtocolTimestamp;
+ stampEnd: DbProtocolTimestamp;
signkeyPub: EddsaPublicKeyString;
masterSig: EddsaSignatureString;
@@ -488,7 +570,7 @@ export interface ExchangeDetailsRecord {
tosAccepted:
| {
etag: string;
- timestamp: TalerPreciseTimestamp;
+ timestamp: DbPreciseTimestamp;
}
| undefined;
@@ -500,25 +582,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;
@@ -528,7 +591,7 @@ export interface ExchangeDetailsPointer {
* Timestamp when the (masterPublicKey, currency) pointer
* has been updated.
*/
- updateClock: TalerPreciseTimestamp;
+ updateClock: DbPreciseTimestamp;
}
export enum ExchangeEntryDbRecordStatus {
@@ -549,11 +612,6 @@ export enum ExchangeEntryDbUpdateStatus {
}
/**
- * Timestamp stored as a IEEE 754 double, in milliseconds.
- */
-export type DbIndexableTimestampMs = number;
-
-/**
* Exchange record as stored in the wallet's database.
*/
export interface ExchangeEntryRecord {
@@ -563,11 +621,17 @@ export interface ExchangeEntryRecord {
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?: TalerPreciseTimestamp;
+ lastWithdrawal?: DbPreciseTimestamp;
/**
* Pointer to the current exchange details.
@@ -588,17 +652,12 @@ export interface ExchangeEntryRecord {
/**
* Last time when the exchange /keys info was updated.
*/
- lastUpdate: TalerPreciseTimestamp | undefined;
+ lastUpdate: DbPreciseTimestamp | undefined;
/**
* Next scheduled update for the exchange.
- *
- * (This field must always be present, so we can index on the timestamp.)
- *
- * FIXME: To index on the timestamp, this needs to be a number of
- * binary timestamp!
*/
- nextUpdateStampMs: DbIndexableTimestampMs;
+ nextUpdateStamp: DbPreciseTimestamp;
lastKeysEtag: string | undefined;
@@ -608,7 +667,7 @@ export interface ExchangeEntryRecord {
* Updated whenever the exchange's denominations are updated or when
* the refresh check has been done.
*/
- nextRefreshCheckStampMs: DbIndexableTimestampMs;
+ nextRefreshCheckStamp: DbPreciseTimestamp;
/**
* Public key of the reserve that we're currently using for
@@ -823,7 +882,7 @@ 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: TalerPreciseTimestamp | undefined;
+ acceptedTimestamp: DbPreciseTimestamp | undefined;
/**
* The tipped amount.
@@ -838,7 +897,7 @@ export interface RewardRecord {
/**
* Timestamp, the tip can't be picked up anymore after this deadline.
*/
- rewardExpiration: TalerProtocolTimestamp;
+ rewardExpiration: DbProtocolTimestamp;
/**
* The exchange that will sign our coins, chosen by the merchant.
@@ -876,7 +935,7 @@ export interface RewardRecord {
*/
merchantRewardId: string;
- createdTimestamp: TalerPreciseTimestamp;
+ createdTimestamp: DbPreciseTimestamp;
/**
* The url to be redirected after the tip is accepted.
@@ -887,7 +946,7 @@ export interface RewardRecord {
* Timestamp for when the wallet finished picking up the tip
* from the merchant.
*/
- pickedUpTimestamp: TalerPreciseTimestamp | undefined;
+ pickedUpTimestamp: DbPreciseTimestamp | undefined;
status: RewardRecordStatus;
}
@@ -985,12 +1044,12 @@ export interface RefreshGroupRecord {
*/
statusPerCoin: RefreshCoinStatus[];
- timestampCreated: TalerPreciseTimestamp;
+ timestampCreated: DbPreciseTimestamp;
/**
* Timestamp when the refresh session finished.
*/
- timestampFinished: TalerPreciseTimestamp | undefined;
+ timestampFinished: DbPreciseTimestamp | undefined;
}
/**
@@ -1215,7 +1274,7 @@ export interface PurchaseRecord {
* Timestamp of the first time that sending a payment to the merchant
* for this purchase was successful.
*/
- timestampFirstSuccessfulPay: TalerPreciseTimestamp | undefined;
+ timestampFirstSuccessfulPay: DbPreciseTimestamp | undefined;
merchantPaySig: string | undefined;
@@ -1230,19 +1289,19 @@ export interface PurchaseRecord {
/**
* When was the purchase record created?
*/
- timestamp: TalerPreciseTimestamp;
+ timestamp: DbPreciseTimestamp;
/**
* When was the purchase made?
* Refers to the time that the user accepted.
*/
- timestampAccept: TalerPreciseTimestamp | undefined;
+ timestampAccept: DbPreciseTimestamp | undefined;
/**
* When was the last refund made?
* Set to 0 if no refund was made on the purchase.
*/
- timestampLastRefundStatus: TalerPreciseTimestamp | undefined;
+ timestampLastRefundStatus: DbPreciseTimestamp | undefined;
/**
* Last session signature that we submitted to /pay (if any).
@@ -1252,7 +1311,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
@@ -1292,12 +1351,12 @@ export interface WalletBackupConfState {
/**
* Timestamp stored in the last backup.
*/
- lastBackupTimestamp?: TalerPreciseTimestamp;
+ lastBackupTimestamp?: DbPreciseTimestamp;
/**
* Last time we tried to do a backup.
*/
- lastBackupCheckTimestamp?: TalerPreciseTimestamp;
+ lastBackupCheckTimestamp?: DbPreciseTimestamp;
lastBackupNonce?: string;
}
@@ -1405,12 +1464,12 @@ export interface WithdrawalGroupRecord {
* When was the withdrawal operation started started?
* Timestamp in milliseconds.
*/
- timestampStart: TalerPreciseTimestamp;
+ timestampStart: DbPreciseTimestamp;
/**
* When was the withdrawal operation completed?
*/
- timestampFinish?: TalerPreciseTimestamp;
+ timestampFinish?: DbPreciseTimestamp;
/**
* Current status of the reserve.
@@ -1501,9 +1560,9 @@ export interface RecoupGroupRecord {
exchangeBaseUrl: string;
- timestampStarted: TalerPreciseTimestamp;
+ timestampStarted: DbPreciseTimestamp;
- timestampFinished: TalerPreciseTimestamp | undefined;
+ timestampFinished: DbPreciseTimestamp | undefined;
/**
* Public keys that identify the coins being recouped
@@ -1537,7 +1596,7 @@ export type BackupProviderState =
}
| {
tag: BackupProviderStateTag.Ready;
- nextBackupTimestamp: TalerPreciseTimestamp;
+ nextBackupTimestamp: DbPreciseTimestamp;
}
| {
tag: BackupProviderStateTag.Retrying;
@@ -1582,7 +1641,7 @@ export interface BackupProviderRecord {
* Does NOT correspond to the timestamp of the backup,
* which only changes when the backup content changes.
*/
- lastBackupCycleTimestamp?: TalerPreciseTimestamp;
+ lastBackupCycleTimestamp?: DbPreciseTimestamp;
/**
* Proposal that we're currently trying to pay for.
@@ -1633,7 +1692,7 @@ export interface DepositTrackingInfo {
// Raw wire transfer identifier of the deposit.
wireTransferId: string;
// When was the wire transfer given to the bank.
- timestampExecuted: TalerProtocolTimestamp;
+ timestampExecuted: DbProtocolTimestamp;
// Total amount transfer for this wtid (including fees)
amountRaw: AmountString;
// Wire fee amount for this exchange
@@ -1655,7 +1714,7 @@ export interface DepositGroupRecord {
*/
amount: AmountString;
- wireTransferDeadline: TalerProtocolTimestamp;
+ wireTransferDeadline: DbProtocolTimestamp;
merchantPub: string;
merchantPriv: string;
@@ -1685,9 +1744,9 @@ export interface DepositGroupRecord {
*/
counterpartyEffectiveDepositAmount: AmountString;
- timestampCreated: TalerPreciseTimestamp;
+ timestampCreated: DbPreciseTimestamp;
- timestampFinished: TalerPreciseTimestamp | undefined;
+ timestampFinished: DbPreciseTimestamp | undefined;
operationStatus: DepositOperationStatus;
@@ -1796,9 +1855,9 @@ export interface PeerPushDebitRecord {
*/
contractEncNonce: string;
- purseExpiration: TalerProtocolTimestamp;
+ purseExpiration: DbProtocolTimestamp;
- timestampCreated: TalerPreciseTimestamp;
+ timestampCreated: DbPreciseTimestamp;
abortRefreshGroupId?: string;
@@ -1871,7 +1930,7 @@ export interface PeerPullCreditRecord {
contractEncNonce: string;
- mergeTimestamp: TalerPreciseTimestamp;
+ mergeTimestamp: DbPreciseTimestamp;
mergeReserveRowId: number;
@@ -1923,7 +1982,7 @@ export interface PeerPushPaymentIncomingRecord {
contractPriv: string;
- timestamp: TalerPreciseTimestamp;
+ timestamp: DbPreciseTimestamp;
estimatedAmountEffective: AmountString;
@@ -1995,7 +2054,7 @@ export interface PeerPullPaymentIncomingRecord {
contractTermsHash: string;
- timestampCreated: TalerPreciseTimestamp;
+ timestampCreated: DbPreciseTimestamp;
/**
* Contract priv that we got from the other party.
@@ -2042,7 +2101,7 @@ export interface OperationRetryRecord {
lastError?: TalerErrorDetail;
- retryInfo: RetryInfo;
+ retryInfo: DbRetryInfo;
}
/**
@@ -2095,14 +2154,13 @@ export interface UserAttentionRecord {
/**
* When the notification was created.
- * FIXME: This should be a TalerPreciseTimestamp
*/
- createdMs: number;
+ created: DbPreciseTimestamp;
/**
* When the user mark this notification as read.
*/
- read: TalerPreciseTimestamp | undefined;
+ read: DbPreciseTimestamp | undefined;
}
export interface DbExchangeHandle {
@@ -2146,7 +2204,7 @@ export interface RefundGroupRecord {
/**
* Timestamp when the refund group was created.
*/
- timestampCreated: TalerPreciseTimestamp;
+ timestampCreated: DbPreciseTimestamp;
proposalId: string;
@@ -2198,12 +2256,12 @@ export interface RefundItemRecord {
/**
* Execution time as claimed by the merchant
*/
- executionTime: TalerProtocolTimestamp;
+ executionTime: DbProtocolTimestamp;
/**
* Time when the wallet became aware of the refund.
*/
- obtainedTime: TalerPreciseTimestamp;
+ obtainedTime: DbPreciseTimestamp;
refundAmount: AmountString;
@@ -2389,15 +2447,19 @@ export const WalletStoresV1 = {
"planchets",
describeContents<PlanchetRecord>({ keyPath: "coinPub" }),
{
- byGroupAgeCoin: describeIndex("byGroupAgeCoin", [
- "withdrawalGroupId",
- "ageWithdrawIdx",
- "coinIdx",
- ]),
- byGroupAndIndex: describeIndex("byGroupAndIndex", [
- "withdrawalGroupId",
- "coinIdx",
- ]),
+ byGroupAgeCoin: describeIndex(
+ "byGroupAgeCoin", [ "withdrawalGroupId", "ageWithdrawIdx", "coinIdx" ],
+ {
+ unique: true,
+ },
+ ),
+ byGroupAndIndex: describeIndex(
+ "byGroupAndIndex",
+ ["withdrawalGroupId", "coinIdx"],
+ {
+ unique: true,
+ },
+ ),
byGroup: describeIndex("byGroup", "withdrawalGroupId"),
byCoinEvHash: describeIndex("byCoinEv", "coinEvHash"),
},
@@ -3049,6 +3111,7 @@ export async function openTalerDatabase(
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
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
index d70eab888..4fc890788 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -31,7 +31,7 @@ import {
AmountJson,
Amounts,
AmountString,
- BankAccessApiClient,
+ TalerCorebankApiClient,
codecForAny,
codecForBankWithdrawalOperationPostResponse,
codecForBatchDepositSuccess,
@@ -109,7 +109,7 @@ export async function checkReserve(
export interface TopupReserveWithDemobankArgs {
http: HttpRequestLibrary;
reservePub: string;
- bankAccessApiBaseUrl: string;
+ corebankApiBaseUrl: string;
exchangeInfo: ExchangeInfo;
amount: AmountString;
}
@@ -117,8 +117,8 @@ export interface TopupReserveWithDemobankArgs {
export async function topupReserveWithDemobank(
args: TopupReserveWithDemobankArgs,
) {
- const { http, bankAccessApiBaseUrl, amount, exchangeInfo, reservePub } = args;
- const bankClient = new BankAccessApiClient(bankAccessApiBaseUrl);
+ const { http, corebankApiBaseUrl, amount, exchangeInfo, reservePub } = args;
+ const bankClient = new TalerCorebankApiClient(corebankApiBaseUrl);
const bankUser = await bankClient.createRandomBankUser();
const wopi = await bankClient.createWithdrawalOperation(
bankUser.username,
@@ -142,7 +142,9 @@ export async function topupReserveWithDemobank(
httpResp,
codecForBankWithdrawalOperationPostResponse(),
);
- await bankClient.confirmWithdrawalOperation(bankUser.username, wopi);
+ await bankClient.confirmWithdrawalOperation(bankUser.username, {
+ withdrawalOperationId: wopi.withdrawal_id,
+ });
}
export async function withdrawCoin(args: {
@@ -276,7 +278,10 @@ export async function depositCoin(args: {
merchant_pub: merchantPub,
};
const url = new URL(`batch-deposit`, dp.exchange_url);
- const httpResp = await http.fetch(url.href, { body: requestBody });
+ const httpResp = await http.fetch(url.href, {
+ method: "POST",
+ body: requestBody,
+ });
await readSuccessResponseJsonOrThrow(httpResp, codecForBatchDepositSuccess());
}
diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts
index a6dae58a1..33162ec50 100644
--- a/packages/taler-wallet-core/src/host-impl.node.ts
+++ b/packages/taler-wallet-core/src/host-impl.node.ts
@@ -108,10 +108,13 @@ async function makeSqliteDb(
filename: args.persistentStoragePath ?? ":memory:",
});
myBackend.enableTracing = false;
+ if (process.env.TALER_WALLET_DBSTATS) {
+ myBackend.trackStats = true;
+ }
const myBridgeIdbFactory = new BridgeIDBFactory(myBackend);
return {
getStats() {
- throw Error("not implemented");
+ return myBackend.accessStats;
},
idbFactory: myBridgeIdbFactory,
};
diff --git a/packages/taler-wallet-core/src/operations/attention.ts b/packages/taler-wallet-core/src/operations/attention.ts
index 7d84b43ef..92d69e93e 100644
--- a/packages/taler-wallet-core/src/operations/attention.ts
+++ b/packages/taler-wallet-core/src/operations/attention.ts
@@ -31,6 +31,7 @@ import {
UserAttentionUnreadList,
} from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
+import { timestampPreciseFromDb, timestampPreciseToDb } from "../index.js";
const logger = new Logger("operations/attention.ts");
@@ -74,7 +75,7 @@ export async function getUserAttentions(
return;
pending.push({
info: x.info,
- when: TalerPreciseTimestamp.fromMilliseconds(x.createdMs),
+ when: timestampPreciseFromDb(x.created),
read: x.read !== undefined,
});
});
@@ -94,7 +95,7 @@ export async function markAttentionRequestAsRead(
if (!ua) throw Error("attention request not found");
tx.userAttention.put({
...ua,
- read: TalerPreciseTimestamp.now(),
+ read: timestampPreciseToDb(TalerPreciseTimestamp.now()),
});
});
}
@@ -117,7 +118,7 @@ export async function addAttentionRequest(
await tx.userAttention.put({
info,
entityId,
- createdMs: AbsoluteTime.now().t_ms as number,
+ created: timestampPreciseToDb(TalerPreciseTimestamp.now()),
read: undefined,
});
});
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts
index a5e8dbd42..7a2771c57 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -84,6 +84,9 @@ import {
ConfigRecord,
ConfigRecordKey,
WalletBackupConfState,
+ timestampOptionalPreciseFromDb,
+ timestampPreciseFromDb,
+ timestampPreciseToDb,
} from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js";
@@ -259,10 +262,12 @@ async function runBackupCycleForProvider(
if (!prov) {
return;
}
- prov.lastBackupCycleTimestamp = TalerPreciseTimestamp.now();
+ prov.lastBackupCycleTimestamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
prov.state = {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: getNextBackupTimestamp(),
+ nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()),
};
await tx.backupProviders.put(prov);
});
@@ -361,10 +366,12 @@ async function runBackupCycleForProvider(
return;
}
prov.lastBackupHash = encodeCrock(currentBackupHash);
- prov.lastBackupCycleTimestamp = TalerPreciseTimestamp.now();
+ prov.lastBackupCycleTimestamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
prov.state = {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: getNextBackupTimestamp(),
+ nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()),
};
await tx.backupProviders.put(prov);
});
@@ -594,7 +601,9 @@ export async function addBackupProvider(
if (req.activate) {
oldProv.state = {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: TalerPreciseTimestamp.now(),
+ nextBackupTimestamp: timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ ),
};
logger.info("setting existing backup provider to active");
await tx.backupProviders.put(oldProv);
@@ -616,7 +625,9 @@ export async function addBackupProvider(
if (req.activate) {
state = {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: TalerPreciseTimestamp.now(),
+ nextBackupTimestamp: timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ ),
};
} else {
state = {
@@ -840,7 +851,9 @@ export async function getBackupInfo(
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
@@ -917,7 +930,9 @@ async function backupRecoveryTheirs(
shouldRetryFreshProposal: false,
state: {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: TalerPreciseTimestamp.now(),
+ nextBackupTimestamp: timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ ),
},
uids: [encodeCrock(getRandomBytes(32))],
});
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts
index 50dd3dc5c..b28a5363d 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -40,6 +40,7 @@ import {
TalerError,
TalerErrorCode,
TalerErrorDetail,
+ TalerPreciseTimestamp,
TombstoneIdStr,
TransactionIdStr,
TransactionType,
@@ -49,6 +50,7 @@ import { CryptoApiStoppedError } from "../crypto/workers/crypto-dispatcher.js";
import {
BackupProviderRecord,
CoinRecord,
+ DbPreciseTimestamp,
DepositGroupRecord,
ExchangeDetailsRecord,
ExchangeEntryDbRecordStatus,
@@ -62,6 +64,7 @@ import {
RecoupGroupRecord,
RefreshGroupRecord,
RewardRecord,
+ timestampPreciseToDb,
WalletStoresV1,
WithdrawalGroupRecord,
} from "../db.js";
@@ -360,11 +363,11 @@ async function storePendingTaskError(
retryRecord = {
id: pendingTaskId,
lastError: e,
- retryInfo: RetryInfo.reset(),
+ retryInfo: DbRetryInfo.reset(),
};
} else {
retryRecord.lastError = e;
- retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
+ retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
}
await tx.operationRetries.put(retryRecord);
return taskToTransactionNotification(ws, tx, pendingTaskId, e);
@@ -383,7 +386,7 @@ export async function resetPendingTaskTimeout(
if (retryRecord) {
// Note that we don't reset the lastError, it should still be visible
// while the retry runs.
- retryRecord.retryInfo = RetryInfo.reset();
+ retryRecord.retryInfo = DbRetryInfo.reset();
await tx.operationRetries.put(retryRecord);
}
return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
@@ -403,14 +406,14 @@ async function storePendingTaskPending(
if (!retryRecord) {
retryRecord = {
id: pendingTaskId,
- retryInfo: RetryInfo.reset(),
+ retryInfo: DbRetryInfo.reset(),
};
} else {
if (retryRecord.lastError) {
hadError = true;
}
delete retryRecord.lastError;
- retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
+ retryRecord.retryInfo = DbRetryInfo.increment(retryRecord.retryInfo);
}
await tx.operationRetries.put(retryRecord);
if (hadError) {
@@ -590,7 +593,7 @@ export function makeExchangeListItem(
return {
exchangeBaseUrl: r.baseUrl,
- currency: exchangeDetails?.currency,
+ currency: exchangeDetails?.currency ?? r.presetCurrencyHint,
exchangeUpdateStatus,
exchangeEntryStatus,
tosStatus: exchangeDetails
@@ -736,9 +739,9 @@ export interface TaskRunLongpollResult {
type: TaskRunResultType.Longpoll;
}
-export interface RetryInfo {
- firstTry: AbsoluteTime;
- nextRetry: AbsoluteTime;
+export interface DbRetryInfo {
+ firstTry: DbPreciseTimestamp;
+ nextRetry: DbPreciseTimestamp;
retryCounter: number;
}
@@ -755,7 +758,7 @@ const defaultRetryPolicy: RetryPolicy = {
};
function updateTimeout(
- r: RetryInfo,
+ r: DbRetryInfo,
p: RetryPolicy = defaultRetryPolicy,
): void {
const now = AbsoluteTime.now();
@@ -763,7 +766,9 @@ function updateTimeout(
throw Error("assertion failed");
}
if (p.backoffDelta.d_ms === "forever") {
- r.nextRetry = AbsoluteTime.never();
+ r.nextRetry = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ );
return;
}
@@ -775,12 +780,12 @@ function updateTimeout(
(p.maxTimeout.d_ms === "forever"
? nextIncrement
: Math.min(p.maxTimeout.d_ms, nextIncrement));
- r.nextRetry = AbsoluteTime.fromMilliseconds(t);
+ r.nextRetry = timestampPreciseToDb(TalerPreciseTimestamp.fromMilliseconds(t));
}
-export namespace RetryInfo {
+export namespace DbRetryInfo {
export function getDuration(
- r: RetryInfo | undefined,
+ r: DbRetryInfo | undefined,
p: RetryPolicy = defaultRetryPolicy,
): Duration {
if (!r) {
@@ -797,11 +802,11 @@ export namespace RetryInfo {
};
}
- export function reset(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
- const now = AbsoluteTime.now();
- const info = {
- firstTry: now,
- nextRetry: now,
+ 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);
@@ -809,9 +814,9 @@ export namespace RetryInfo {
}
export function increment(
- r: RetryInfo | undefined,
+ r: DbRetryInfo | undefined,
p: RetryPolicy = defaultRetryPolicy,
- ): RetryInfo {
+ ): DbRetryInfo {
if (!r) {
return reset(p);
}
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 2de8f30a1..111d15989 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -73,6 +73,9 @@ import {
RefreshOperationStatus,
createRefreshGroup,
getTotalRefreshCost,
+ timestampPreciseToDb,
+ timestampProtocolFromDb,
+ timestampProtocolToDb,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
@@ -799,7 +802,7 @@ async function processDepositGroupPendingTrack(
amountRaw: Amounts.stringify(raw),
wireFee: Amounts.stringify(wireFee),
exchangePub: track.exchange_pub,
- timestampExecuted: track.execution_time,
+ timestampExecuted: timestampProtocolToDb(track.execution_time),
wireTransferId: track.wtid,
},
id: track.exchange_sig,
@@ -857,7 +860,9 @@ async function processDepositGroupPendingTrack(
}
}
if (allWired) {
- dg.timestampFinished = TalerPreciseTimestamp.now();
+ dg.timestampFinished = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
dg.operationStatus = DepositOperationStatus.Finished;
await tx.depositGroups.put(dg);
}
@@ -1375,7 +1380,9 @@ export async function createDepositGroup(
amount: contractData.amount,
noncePriv: noncePair.priv,
noncePub: noncePair.pub,
- timestampCreated: AbsoluteTime.toPreciseTimestamp(now),
+ timestampCreated: timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(now),
+ ),
timestampFinished: undefined,
statusPerCoin: payCoinSel.coinSel.coinPubs.map(
() => DepositElementStatus.DepositPending,
@@ -1388,7 +1395,9 @@ export async function createDepositGroup(
counterpartyEffectiveDepositAmount: Amounts.stringify(
counterpartyEffectiveDepositAmount,
),
- wireTransferDeadline: contractTerms.wire_transfer_deadline,
+ wireTransferDeadline: timestampProtocolToDb(
+ contractTerms.wire_transfer_deadline,
+ ),
wire: {
payto_uri: req.depositPaytoUri,
salt: wireSalt,
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 43a08ed3b..82d7b42bf 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -74,6 +74,9 @@ import {
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
isWithdrawableDenom,
+ timestampPreciseFromDb,
+ timestampPreciseToDb,
+ timestampProtocolToDb,
WalletDbReadWriteTransaction,
} from "../index.js";
import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";
@@ -174,7 +177,7 @@ export async function acceptExchangeTermsOfService(
if (d) {
d.tosAccepted = {
etag: etag || d.tosCurrentEtag,
- timestamp: TalerPreciseTimestamp.now(),
+ timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
await tx.exchangeDetails.put(d);
}
@@ -306,6 +309,7 @@ export async function downloadExchangeInfo(
export async function addPresetExchangeEntry(
tx: WalletDbReadWriteTransaction<"exchanges">,
exchangeBaseUrl: string,
+ currencyHint?: string,
): Promise<void> {
let exchange = await tx.exchanges.get(exchangeBaseUrl);
if (!exchange) {
@@ -313,17 +317,22 @@ export async function addPresetExchangeEntry(
entryStatus: ExchangeEntryDbRecordStatus.Preset,
updateStatus: ExchangeEntryDbUpdateStatus.Initial,
baseUrl: exchangeBaseUrl,
+ presetCurrencyHint: currencyHint,
detailsPointer: undefined,
lastUpdate: undefined,
lastKeysEtag: undefined,
- nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(),
- nextUpdateStampMs: AbsoluteTime.getStampMsNever(),
+ nextRefreshCheckStamp: timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ ),
+ nextUpdateStamp: timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ ),
};
await tx.exchanges.put(r);
}
}
-export async function provideExchangeRecordInTx(
+async function provideExchangeRecordInTx(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
exchanges: typeof WalletStoresV1.exchanges;
@@ -343,8 +352,12 @@ export async function provideExchangeRecordInTx(
baseUrl: baseUrl,
detailsPointer: undefined,
lastUpdate: undefined,
- nextUpdateStampMs: AbsoluteTime.getStampMsNever(),
- nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(),
+ nextUpdateStamp: timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ ),
+ nextRefreshCheckStamp: timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ ),
lastKeysEtag: undefined,
};
await tx.exchanges.put(r);
@@ -445,13 +458,19 @@ async function downloadExchangeKeysInfo(
isRevoked: false,
value: Amounts.stringify(value),
currency: value.currency,
- stampExpireDeposit: denomIn.stamp_expire_deposit,
- stampExpireLegal: denomIn.stamp_expire_legal,
- stampExpireWithdraw: denomIn.stamp_expire_withdraw,
- stampStart: denomIn.stamp_start,
+ 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,
- listIssueDate: exchangeKeysJsonUnchecked.list_issue_date,
+ listIssueDate: timestampProtocolToDb(
+ exchangeKeysJsonUnchecked.list_issue_date,
+ ),
fees: {
feeDeposit: Amounts.stringify(denomGroup.fee_deposit),
feeRefresh: Amounts.stringify(denomGroup.fee_refresh),
@@ -613,7 +632,9 @@ export async function updateExchangeFromUrlHandler(
!forceNow &&
exchangeDetails !== undefined &&
!AbsoluteTime.isExpired(
- AbsoluteTime.fromStampMs(exchange.nextUpdateStampMs),
+ AbsoluteTime.fromPreciseTimestamp(
+ timestampPreciseFromDb(exchange.nextUpdateStamp),
+ ),
)
) {
logger.trace("using existing exchange info");
@@ -753,17 +774,21 @@ export async function updateExchangeFromUrlHandler(
if (existingDetails?.rowId) {
newDetails.rowId = existingDetails.rowId;
}
- r.lastUpdate = TalerPreciseTimestamp.now();
- r.nextUpdateStampMs = AbsoluteTime.toStampMs(
- AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry),
+ r.lastUpdate = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ r.nextUpdateStamp = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(
+ AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry),
+ ),
);
// New denominations might be available.
- r.nextRefreshCheckStampMs = AbsoluteTime.getStampMsNow();
+ r.nextRefreshCheckStamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
if (detailsPointerChanged) {
r.detailsPointer = {
currency: newDetails.currency,
masterPublicKey: newDetails.masterPublicKey,
- updateClock: TalerPreciseTimestamp.now(),
+ updateClock: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
}
await tx.exchanges.put(r);
@@ -776,9 +801,9 @@ export async function updateExchangeFromUrlHandler(
exchangeDetailsRowId: drRowId.key,
masterSig: sk.master_sig,
signkeyPub: sk.key,
- stampEnd: sk.stamp_end,
- stampExpire: sk.stamp_expire,
- stampStart: sk.stamp_start,
+ stampEnd: timestampProtocolToDb(sk.stamp_end),
+ stampExpire: timestampProtocolToDb(sk.stamp_expire),
+ stampStart: timestampProtocolToDb(sk.stamp_start),
});
}
@@ -813,7 +838,7 @@ export async function updateExchangeFromUrlHandler(
);
}
} else {
- x.listIssueDate = keysInfo.listIssueDate;
+ x.listIssueDate = timestampProtocolToDb(keysInfo.listIssueDate);
if (!x.isOffered) {
x.isOffered = true;
logger.info(
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index fe0cbeda0..157541ed3 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -103,6 +103,9 @@ import {
RefundGroupStatus,
RefundItemRecord,
RefundItemStatus,
+ timestampPreciseToDb,
+ timestampProtocolFromDb,
+ timestampProtocolToDb,
} from "../index.js";
import {
EXCHANGE_COINS_LOCK,
@@ -114,7 +117,7 @@ import { checkDbInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess } from "../util/query.js";
import {
constructTaskIdentifier,
- RetryInfo,
+ DbRetryInfo,
runLongpollAsync,
runTaskWithErrorReporting,
spendCoins,
@@ -216,11 +219,13 @@ async function failProposalPermanently(
notifyTransition(ws, transactionId, transitionInfo);
}
-function getProposalRequestTimeout(retryInfo?: RetryInfo): Duration {
+function getProposalRequestTimeout(retryInfo?: DbRetryInfo): Duration {
return Duration.clamp({
lower: Duration.fromSpec({ seconds: 1 }),
upper: Duration.fromSpec({ seconds: 60 }),
- value: retryInfo ? RetryInfo.getDuration(retryInfo) : Duration.fromSpec({}),
+ value: retryInfo
+ ? DbRetryInfo.getDuration(retryInfo)
+ : Duration.fromSpec({}),
});
}
@@ -644,7 +649,7 @@ async function createPurchase(
noncePriv: priv,
noncePub: pub,
claimToken,
- timestamp: TalerPreciseTimestamp.now(),
+ timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
merchantBaseUrl,
orderId,
proposalId: proposalId,
@@ -717,7 +722,7 @@ async function storeFirstPaySuccess(
if (purchase.purchaseStatus === PurchaseStatus.PendingPaying) {
purchase.purchaseStatus = PurchaseStatus.Done;
}
- purchase.timestampFirstSuccessfulPay = now;
+ purchase.timestampFirstSuccessfulPay = timestampPreciseToDb(now);
purchase.lastSessionId = sessionId;
purchase.merchantPaySig = payResponse.sig;
purchase.posConfirmation = payResponse.pos_confirmation;
@@ -737,8 +742,10 @@ async function storeFirstPaySuccess(
const ar = Duration.fromTalerProtocolDuration(protoAr);
logger.info("auto_refund present");
purchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund;
- purchase.autoRefundDeadline = AbsoluteTime.toProtocolTimestamp(
- AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
+ purchase.autoRefundDeadline = timestampProtocolToDb(
+ AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
+ ),
);
}
await tx.purchases.put(purchase);
@@ -941,7 +948,9 @@ async function unblockBackup(
.forEachAsync(async (bp) => {
bp.state = {
tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: TalerPreciseTimestamp.now(),
+ nextBackupTimestamp: timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ ),
};
tx.backupProviders.put(bp);
});
@@ -1447,7 +1456,7 @@ export async function confirmPay(
totalPayCost: Amounts.stringify(payCostInfo),
};
p.lastSessionId = sessionId;
- p.timestampAccept = TalerPreciseTimestamp.now();
+ p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now());
p.purchaseStatus = PurchaseStatus.PendingPaying;
await tx.purchases.put(p);
await spendCoins(ws, tx, {
@@ -2340,7 +2349,9 @@ async function processPurchaseAutoRefund(
if (
!purchase.autoRefundDeadline ||
AbsoluteTime.isExpired(
- AbsoluteTime.fromProtocolTimestamp(purchase.autoRefundDeadline),
+ AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(purchase.autoRefundDeadline),
+ ),
)
) {
const transitionInfo = await ws.db
@@ -2791,7 +2802,7 @@ async function storeRefunds(
proposalId: purchase.proposalId,
refundGroupId: newRefundGroupId,
status: RefundGroupStatus.Pending,
- timestampCreated: now,
+ timestampCreated: timestampPreciseToDb(now),
amountEffective: Amounts.stringify(
Amounts.zeroOfCurrency(currency),
),
@@ -2801,8 +2812,8 @@ async function storeRefunds(
const status: RefundItemStatus = getItemStatus(rf);
const newItem: RefundItemRecord = {
coinPub: rf.coin_pub,
- executionTime: rf.execution_time,
- obtainedTime: now,
+ executionTime: timestampProtocolToDb(rf.execution_time),
+ obtainedTime: timestampPreciseToDb(now),
refundAmount: rf.refund_amount,
refundGroupId: newGroup.refundGroupId,
rtxid: rf.rtransaction_id,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
index 0355eb152..54b78957f 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
@@ -60,6 +60,9 @@ import {
PeerPullPaymentCreditStatus,
WithdrawalGroupStatus,
WithdrawalRecordType,
+ timestampOptionalPreciseFromDb,
+ timestampPreciseFromDb,
+ timestampPreciseToDb,
updateExchangeFromUrl,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
@@ -389,18 +392,20 @@ async function handlePeerPullCreditCreatePurse(
const econtractResp = await ws.cryptoApi.encryptContractForDeposit({
contractPriv: pullIni.contractPriv,
contractPub: pullIni.contractPub,
- contractTerms: contractTermsRecord,
+ 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 ws.cryptoApi.signReservePurseCreate({
contractTermsHash: pullIni.contractTermsHash,
flags: WalletAccountMergeFlags.CreateWithPurseFee,
mergePriv: pullIni.mergePriv,
- mergeTimestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp),
+ mergeTimestamp: TalerPreciseTimestamp.round(mergeTimestamp),
purseAmount: pullIni.amount,
purseExpiration: purseExpiration,
purseFee: purseFee,
@@ -412,7 +417,7 @@ async function handlePeerPullCreditCreatePurse(
const reservePurseReqBody: ExchangeReservePurseRequest = {
merge_sig: sigRes.mergeSig,
- merge_timestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp),
+ merge_timestamp: TalerPreciseTimestamp.round(mergeTimestamp),
h_contract_terms: pullIni.contractTermsHash,
merge_pub: pullIni.mergePub,
min_age: 0,
@@ -695,11 +700,17 @@ async function getPreferredExchangeForCurrency(
if (candidate.lastWithdrawal && !e.lastWithdrawal) {
continue;
}
- if (candidate.lastWithdrawal && e.lastWithdrawal) {
+ const exchangeLastWithdrawal = timestampOptionalPreciseFromDb(
+ e.lastWithdrawal,
+ );
+ const candidateLastWithdrawal = timestampOptionalPreciseFromDb(
+ candidate.lastWithdrawal,
+ );
+ if (exchangeLastWithdrawal && candidateLastWithdrawal) {
if (
AbsoluteTime.cmp(
- AbsoluteTime.fromPreciseTimestamp(e.lastWithdrawal),
- AbsoluteTime.fromPreciseTimestamp(candidate.lastWithdrawal),
+ AbsoluteTime.fromPreciseTimestamp(exchangeLastWithdrawal),
+ AbsoluteTime.fromPreciseTimestamp(candidateLastWithdrawal),
) > 0
) {
candidate = e;
@@ -741,8 +752,6 @@ export async function initiatePeerPullPayment(
exchangeBaseUrl: exchangeBaseUrl,
});
- const mergeTimestamp = TalerPreciseTimestamp.now();
-
const pursePair = await ws.cryptoApi.createEddsaKeypair({});
const mergePair = await ws.cryptoApi.createEddsaKeypair({});
@@ -766,6 +775,8 @@ export async function initiatePeerPullPayment(
undefined,
);
+ const mergeTimestamp = TalerPreciseTimestamp.now();
+
const transitionInfo = await ws.db
.mktx((x) => [x.peerPullCredit, x.contractTerms])
.runReadWrite(async (tx) => {
@@ -778,7 +789,7 @@ export async function initiatePeerPullPayment(
mergePriv: mergePair.priv,
mergePub: mergePair.pub,
status: PeerPullPaymentCreditStatus.PendingCreatePurse,
- mergeTimestamp,
+ mergeTimestamp: timestampPreciseToDb(mergeTimestamp),
contractEncNonce,
mergeReserveRowId: mergeReserveRowId,
contractPriv: contractKeyPair.priv,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
index 5bcfa3418..48cbf574f 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
@@ -59,6 +59,7 @@ import {
PendingTaskType,
RefreshOperationStatus,
createRefreshGroup,
+ timestampPreciseToDb,
} from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
@@ -595,7 +596,7 @@ export async function preparePeerPullDebit(
contractPriv: contractPriv,
exchangeBaseUrl: exchangeBaseUrl,
pursePub: pursePub,
- timestampCreated: TalerPreciseTimestamp.now(),
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
contractTermsHash,
amount: contractTerms.amount,
status: PeerPullDebitRecordStatus.DialogProposed,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
index 89d9e3b49..e4698c203 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
@@ -59,6 +59,7 @@ import {
PendingTaskType,
WithdrawalGroupStatus,
WithdrawalRecordType,
+ timestampPreciseToDb,
} from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant } from "../util/invariants.js";
@@ -129,12 +130,10 @@ export async function preparePeerPushCredit(
amountEffective: existing.existingPushInc.estimatedAmountEffective,
amountRaw: existing.existingContractTerms.amount,
contractTerms: existing.existingContractTerms,
- peerPushCreditId:
- existing.existingPushInc.peerPushCreditId,
+ peerPushCreditId: existing.existingPushInc.peerPushCreditId,
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
- peerPushCreditId:
- existing.existingPushInc.peerPushCreditId,
+ peerPushCreditId: existing.existingPushInc.peerPushCreditId,
}),
};
}
@@ -196,7 +195,7 @@ export async function preparePeerPushCredit(
exchangeBaseUrl: exchangeBaseUrl,
mergePriv: dec.mergePriv,
pursePub: pursePub,
- timestamp: TalerPreciseTimestamp.now(),
+ timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
contractTermsHash,
status: PeerPushCreditStatus.DialogProposed,
withdrawalGroupId,
@@ -263,16 +262,11 @@ async function longpollKycStatus(
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushCredit])
.runReadWrite(async (tx) => {
- const peerInc = await tx.peerPushCredit.get(
- peerPushCreditId,
- );
+ const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
return;
}
- if (
- peerInc.status !==
- PeerPushCreditStatus.PendingMergeKycRequired
- ) {
+ if (peerInc.status !== PeerPushCreditStatus.PendingMergeKycRequired) {
return;
}
const oldTxState = computePeerPushCreditTransactionState(peerInc);
@@ -333,9 +327,7 @@ async function processPeerPushCreditKycRequired(
const { transitionInfo, result } = await ws.db
.mktx((x) => [x.peerPushCredit])
.runReadWrite(async (tx) => {
- const peerInc = await tx.peerPushCredit.get(
- peerPushCreditId,
- );
+ const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
return {
transitionInfo: undefined,
@@ -466,9 +458,7 @@ async function handlePendingMerge(
x.exchangeDetails,
])
.runReadWrite(async (tx) => {
- const peerInc = await tx.peerPushCredit.get(
- peerPushCreditId,
- );
+ const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
return undefined;
}
@@ -520,9 +510,7 @@ async function handlePendingWithdrawing(
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushCredit, x.withdrawalGroups])
.runReadWrite(async (tx) => {
- const ppi = await tx.peerPushCredit.get(
- peerInc.peerPushCreditId,
- );
+ const ppi = await tx.peerPushCredit.get(peerInc.peerPushCreditId);
if (!ppi) {
finished = true;
return;
@@ -631,9 +619,7 @@ export async function confirmPeerPushCredit(
}
peerPushCreditId = parsedTx.peerPushCreditId;
} else {
- throw Error(
- "no transaction ID (or deprecated peerPushCreditId) provided",
- );
+ throw Error("no transaction ID (or deprecated peerPushCreditId) provided");
}
await ws.db
@@ -683,9 +669,7 @@ export async function suspendPeerPushCreditTransaction(
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushCredit])
.runReadWrite(async (tx) => {
- const pushCreditRec = await tx.peerPushCredit.get(
- peerPushCreditId,
- );
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
logger.warn(`peer push credit ${peerPushCreditId} not found`);
return;
@@ -746,9 +730,7 @@ export async function abortPeerPushCreditTransaction(
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushCredit])
.runReadWrite(async (tx) => {
- const pushCreditRec = await tx.peerPushCredit.get(
- peerPushCreditId,
- );
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
logger.warn(`peer push credit ${peerPushCreditId} not found`);
return;
@@ -820,9 +802,7 @@ export async function resumePeerPushCreditTransaction(
const transitionInfo = await ws.db
.mktx((x) => [x.peerPushCredit])
.runReadWrite(async (tx) => {
- const pushCreditRec = await tx.peerPushCredit.get(
- peerPushCreditId,
- );
+ const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
logger.warn(`peer push credit ${peerPushCreditId} not found`);
return;
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
index e80ffc059..50ae8d41b 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
@@ -55,10 +55,14 @@ import {
PeerPushDebitStatus,
RefreshOperationStatus,
createRefreshGroup,
+ timestampPreciseToDb,
+ timestampProtocolFromDb,
+ timestampProtocolToDb,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
+import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
import { checkLogicInvariant } from "../util/invariants.js";
import {
TaskRunResult,
@@ -77,7 +81,6 @@ import {
notifyTransition,
stopLongpolling,
} from "./transactions.js";
-import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
const logger = new Logger("pay-peer-push-debit.ts");
@@ -207,7 +210,7 @@ async function processPeerPushDebitCreateReserve(
mergePub: peerPushInitiation.mergePub,
minAge: 0,
purseAmount: peerPushInitiation.amount,
- purseExpiration,
+ purseExpiration: timestampProtocolFromDb(purseExpiration),
pursePriv: peerPushInitiation.pursePriv,
});
@@ -242,8 +245,6 @@ async function processPeerPushDebitCreateReserve(
hash(decodeCrock(econtractResp.econtract.econtract)),
);
- logger.info(`econtract hash: ${econtractHash}`);
-
const createPurseUrl = new URL(
`purses/${peerPushInitiation.pursePub}/create`,
peerPushInitiation.exchangeBaseUrl,
@@ -254,7 +255,7 @@ async function processPeerPushDebitCreateReserve(
merge_pub: peerPushInitiation.mergePub,
purse_sig: purseSigResp.sig,
h_contract_terms: hContractTerms,
- purse_expiration: purseExpiration,
+ purse_expiration: timestampProtocolFromDb(purseExpiration),
deposits: depositSigsResp.deposits,
min_age: 0,
econtract: econtractResp.econtract,
@@ -646,7 +647,6 @@ export async function initiatePeerPushDebit(
// we might want to mark the coins as used and spend them
// after we've been able to create the purse.
await spendCoins(ws, tx, {
- // allocationId: `txn:peer-push-debit:${pursePair.pub}`,
allocationId: constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
pursePub: pursePair.pub,
@@ -666,10 +666,10 @@ export async function initiatePeerPushDebit(
exchangeBaseUrl: sel.exchangeBaseUrl,
mergePriv: mergePair.priv,
mergePub: mergePair.pub,
- purseExpiration: purseExpiration,
+ purseExpiration: timestampProtocolToDb(purseExpiration),
pursePriv: pursePair.priv,
pursePub: pursePair.pub,
- timestampCreated: TalerPreciseTimestamp.now(),
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
status: PeerPushDebitStatus.PendingCreatePurse,
contractEncNonce,
coinSel: {
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index 6115f848b..1819aa1b8 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -21,42 +21,46 @@
/**
* Imports.
*/
+import { GlobalIDB } from "@gnu-taler/idb-bridge";
+import { AbsoluteTime, TransactionRecordFilter } from "@gnu-taler/taler-util";
import {
- PurchaseStatus,
- WalletStoresV1,
BackupProviderStateTag,
- RefreshCoinStatus,
- PeerPushDebitStatus,
- PeerPullDebitRecordStatus,
- PeerPushCreditStatus,
- PeerPullPaymentCreditStatus,
- WithdrawalGroupStatus,
- RewardRecordStatus,
- DepositOperationStatus,
- RefreshGroupRecord,
- WithdrawalGroupRecord,
+ DepositElementStatus,
DepositGroupRecord,
- RewardRecord,
- PurchaseRecord,
+ DepositOperationStatus,
+ ExchangeEntryDbUpdateStatus,
PeerPullCreditRecord,
+ PeerPullDebitRecordStatus,
+ PeerPullPaymentCreditStatus,
PeerPullPaymentIncomingRecord,
+ PeerPushCreditStatus,
PeerPushDebitRecord,
+ PeerPushDebitStatus,
PeerPushPaymentIncomingRecord,
+ PurchaseRecord,
+ PurchaseStatus,
+ RefreshCoinStatus,
+ RefreshGroupRecord,
+ RefreshOperationStatus,
RefundGroupRecord,
RefundGroupStatus,
- ExchangeEntryDbUpdateStatus,
- RefreshOperationStatus,
- DepositElementStatus,
+ RewardRecord,
+ RewardRecordStatus,
+ WalletStoresV1,
+ WithdrawalGroupRecord,
+ WithdrawalGroupStatus,
+ timestampAbsoluteFromDb,
+ timestampOptionalAbsoluteFromDb,
+ timestampPreciseFromDb,
+ timestampPreciseToDb,
} from "../db.js";
+import { InternalWalletState } from "../internal-wallet-state.js";
import {
PendingOperationsResponse,
PendingTaskType,
TaskId,
} from "../pending-types.js";
-import { AbsoluteTime, TransactionRecordFilter } from "@gnu-taler/taler-util";
-import { InternalWalletState } from "../internal-wallet-state.js";
import { GetReadOnlyAccess } from "../util/query.js";
-import { GlobalIDB } from "@gnu-taler/idb-bridge";
import { TaskIdentifiers } from "./common.js";
function getPendingCommon(
@@ -99,12 +103,14 @@ async function gatherExchangePending(
}
const opTag = TaskIdentifiers.forExchangeUpdate(exch);
let opr = await tx.operationRetries.get(opTag);
- const timestampDue =
- opr?.retryInfo.nextRetry ??
- AbsoluteTime.fromStampMs(exch.nextUpdateStampMs);
+ const timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp;
resp.pendingOperations.push({
type: PendingTaskType.ExchangeUpdate,
- ...getPendingCommon(ws, opTag, timestampDue),
+ ...getPendingCommon(
+ ws,
+ opTag,
+ AbsoluteTime.fromPreciseTimestamp(timestampPreciseFromDb(timestampDue)),
+ ),
givesLifeness: false,
exchangeBaseUrl: exch.baseUrl,
lastError: opr?.lastError,
@@ -115,8 +121,16 @@ async function gatherExchangePending(
if (!opr?.lastError) {
resp.pendingOperations.push({
type: PendingTaskType.ExchangeCheckRefresh,
- ...getPendingCommon(ws, opTag, timestampDue),
- timestampDue: AbsoluteTime.fromStampMs(exch.nextRefreshCheckStampMs),
+ ...getPendingCommon(
+ ws,
+ opTag,
+ AbsoluteTime.fromPreciseTimestamp(
+ timestampPreciseFromDb(timestampDue),
+ ),
+ ),
+ timestampDue: AbsoluteTime.fromPreciseTimestamp(
+ timestampPreciseFromDb(exch.nextRefreshCheckStamp),
+ ),
givesLifeness: false,
exchangeBaseUrl: exch.baseUrl,
});
@@ -165,7 +179,9 @@ async function gatherRefreshPending(
}
const opId = TaskIdentifiers.forRefresh(r);
const retryRecord = await tx.operationRetries.get(opId);
- const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
+ const timestampDue =
+ timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ??
+ AbsoluteTime.now();
resp.pendingOperations.push({
type: PendingTaskType.Refresh,
...getPendingCommon(ws, opId, timestampDue),
@@ -222,8 +238,8 @@ async function gatherWithdrawalPending(
opr = {
id: opTag,
retryInfo: {
- firstTry: now,
- nextRetry: now,
+ firstTry: timestampPreciseToDb(AbsoluteTime.toPreciseTimestamp(now)),
+ nextRetry: timestampPreciseToDb(AbsoluteTime.toPreciseTimestamp(now)),
retryCounter: 0,
},
};
@@ -233,7 +249,8 @@ async function gatherWithdrawalPending(
...getPendingCommon(
ws,
opTag,
- opr.retryInfo?.nextRetry ?? AbsoluteTime.now(),
+ timestampOptionalAbsoluteFromDb(opr.retryInfo?.nextRetry) ??
+ AbsoluteTime.now(),
),
givesLifeness: true,
withdrawalGroupId: wsr.withdrawalGroupId,
@@ -285,7 +302,9 @@ async function gatherDepositPending(
}
const opId = TaskIdentifiers.forDeposit(dg);
const retryRecord = await tx.operationRetries.get(opId);
- const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
+ const timestampDue =
+ timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ??
+ AbsoluteTime.now();
resp.pendingOperations.push({
type: PendingTaskType.Deposit,
...getPendingCommon(ws, opId, timestampDue),
@@ -330,13 +349,15 @@ async function gatherRewardPending(
await iterRecordsForReward(tx, { onlyState: "nonfinal" }, async (tip) => {
const opId = TaskIdentifiers.forTipPickup(tip);
const retryRecord = await tx.operationRetries.get(opId);
- const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
+ const timestampDue =
+ timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ??
+ AbsoluteTime.now();
if (tip.acceptedTimestamp) {
resp.pendingOperations.push({
type: PendingTaskType.RewardPickup,
...getPendingCommon(ws, opId, timestampDue),
givesLifeness: true,
- timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(),
+ timestampDue,
merchantBaseUrl: tip.merchantBaseUrl,
tipId: tip.walletRewardId,
merchantTipId: tip.merchantRewardId,
@@ -390,7 +411,9 @@ async function gatherPurchasePending(
await iterRecordsForPurchase(tx, { onlyState: "nonfinal" }, async (pr) => {
const opId = TaskIdentifiers.forPay(pr);
const retryRecord = await tx.operationRetries.get(opId);
- const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
+ const timestampDue =
+ timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ??
+ AbsoluteTime.now();
resp.pendingOperations.push({
type: PendingTaskType.Purchase,
...getPendingCommon(ws, opId, timestampDue),
@@ -419,7 +442,9 @@ async function gatherRecoupPending(
}
const opId = TaskIdentifiers.forRecoup(rg);
const retryRecord = await tx.operationRetries.get(opId);
- const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
+ const timestampDue =
+ timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ??
+ AbsoluteTime.now();
resp.pendingOperations.push({
type: PendingTaskType.Recoup,
...getPendingCommon(ws, opId, timestampDue),
@@ -444,7 +469,7 @@ async function gatherBackupPending(
const opId = TaskIdentifiers.forBackup(bp);
const retryRecord = await tx.operationRetries.get(opId);
if (bp.state.tag === BackupProviderStateTag.Ready) {
- const timestampDue = AbsoluteTime.fromPreciseTimestamp(
+ const timestampDue = timestampAbsoluteFromDb(
bp.state.nextBackupTimestamp,
);
resp.pendingOperations.push({
@@ -456,7 +481,8 @@ async function gatherBackupPending(
});
} else if (bp.state.tag === BackupProviderStateTag.Retrying) {
const timestampDue =
- retryRecord?.retryInfo?.nextRetry ?? AbsoluteTime.now();
+ timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo?.nextRetry) ??
+ AbsoluteTime.now();
resp.pendingOperations.push({
type: PendingTaskType.Backup,
...getPendingCommon(ws, opId, timestampDue),
@@ -503,7 +529,8 @@ async function gatherPeerPullInitiationPending(
const opId = TaskIdentifiers.forPeerPullPaymentInitiation(pi);
const retryRecord = await tx.operationRetries.get(opId);
const timestampDue =
- retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
+ timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ??
+ AbsoluteTime.now();
resp.pendingOperations.push({
type: PendingTaskType.PeerPullCredit,
...getPendingCommon(ws, opId, timestampDue),
@@ -549,7 +576,8 @@ async function gatherPeerPullDebitPending(
const opId = TaskIdentifiers.forPeerPullPaymentDebit(pi);
const retryRecord = await tx.operationRetries.get(opId);
const timestampDue =
- retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
+ timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ??
+ AbsoluteTime.now();
resp.pendingOperations.push({
type: PendingTaskType.PeerPullDebit,
...getPendingCommon(ws, opId, timestampDue),
@@ -595,7 +623,8 @@ async function gatherPeerPushInitiationPending(
const opId = TaskIdentifiers.forPeerPushPaymentInitiation(pi);
const retryRecord = await tx.operationRetries.get(opId);
const timestampDue =
- retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
+ timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ??
+ AbsoluteTime.now();
resp.pendingOperations.push({
type: PendingTaskType.PeerPushDebit,
...getPendingCommon(ws, opId, timestampDue),
@@ -645,7 +674,8 @@ async function gatherPeerPushCreditPending(
const opId = TaskIdentifiers.forPeerPushCredit(pi);
const retryRecord = await tx.operationRetries.get(opId);
const timestampDue =
- retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
+ timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ??
+ AbsoluteTime.now();
resp.pendingOperations.push({
type: PendingTaskType.PeerPushCredit,
...getPendingCommon(ws, opId, timestampDue),
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts
index 6a18e5de6..782e98d1c 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -47,6 +47,7 @@ import {
WithdrawCoinSource,
WithdrawalGroupStatus,
WithdrawalRecordType,
+ timestampPreciseToDb,
} from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
@@ -391,7 +392,7 @@ export async function processRecoupGroup(
if (!rg2) {
return;
}
- rg2.timestampFinished = TalerPreciseTimestamp.now();
+ rg2.timestampFinished = timestampPreciseToDb(TalerPreciseTimestamp.now());
if (rg2.scheduleRefreshCoins.length > 0) {
const refreshGroupId = await createRefreshGroup(
ws,
@@ -424,7 +425,7 @@ export async function createRecoupGroup(
exchangeBaseUrl: exchangeBaseUrl,
coinPubs: coinPubs,
timestampFinished: undefined,
- timestampStarted: TalerPreciseTimestamp.now(),
+ timestampStarted: timestampPreciseToDb(TalerPreciseTimestamp.now()),
recoupFinishedPerCoin: coinPubs.map(() => false),
scheduleRefreshCoins: [],
};
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index 75adbc860..95aedbbd6 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -80,6 +80,8 @@ import {
isWithdrawableDenom,
PendingTaskType,
RefreshSessionRecord,
+ timestampPreciseToDb,
+ timestampProtocolFromDb,
} from "../index.js";
import {
EXCHANGE_COINS_LOCK,
@@ -157,10 +159,10 @@ function updateGroupStatus(rg: RefreshGroupRecord): { final: boolean } {
);
if (allFinal) {
if (anyFailed) {
- rg.timestampFinished = TalerPreciseTimestamp.now();
+ rg.timestampFinished = timestampPreciseToDb(TalerPreciseTimestamp.now());
rg.operationStatus = RefreshOperationStatus.Failed;
} else {
- rg.timestampFinished = TalerPreciseTimestamp.now();
+ rg.timestampFinished = timestampPreciseToDb(TalerPreciseTimestamp.now());
rg.operationStatus = RefreshOperationStatus.Finished;
}
return { final: true };
@@ -1099,12 +1101,14 @@ export async function createRefreshGroup(
expectedOutputPerCoin: estimatedOutputPerCoin.map((x) =>
Amounts.stringify(x),
),
- timestampCreated: TalerPreciseTimestamp.now(),
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
if (oldCoinPubs.length == 0) {
logger.warn("created refresh group with zero coins");
- refreshGroup.timestampFinished = TalerPreciseTimestamp.now();
+ refreshGroup.timestampFinished = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
refreshGroup.operationStatus = RefreshOperationStatus.Finished;
}
@@ -1122,10 +1126,10 @@ export async function createRefreshGroup(
*/
function getAutoRefreshCheckThreshold(d: DenominationRecord): AbsoluteTime {
const expireWithdraw = AbsoluteTime.fromProtocolTimestamp(
- d.stampExpireWithdraw,
+ timestampProtocolFromDb(d.stampExpireWithdraw),
);
const expireDeposit = AbsoluteTime.fromProtocolTimestamp(
- d.stampExpireDeposit,
+ timestampProtocolFromDb(d.stampExpireDeposit),
);
const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit);
const deltaDiv = durationMul(delta, 0.75);
@@ -1137,10 +1141,10 @@ function getAutoRefreshCheckThreshold(d: DenominationRecord): AbsoluteTime {
*/
function getAutoRefreshExecuteThreshold(d: DenominationRecord): AbsoluteTime {
const expireWithdraw = AbsoluteTime.fromProtocolTimestamp(
- d.stampExpireWithdraw,
+ timestampProtocolFromDb(d.stampExpireWithdraw),
);
const expireDeposit = AbsoluteTime.fromProtocolTimestamp(
- d.stampExpireDeposit,
+ timestampProtocolFromDb(d.stampExpireDeposit),
);
const delta = AbsoluteTime.difference(expireWithdraw, expireDeposit);
const deltaDiv = durationMul(delta, 0.5);
@@ -1224,8 +1228,9 @@ export async function autoRefresh(
logger.trace(
`next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`,
);
- exchange.nextRefreshCheckStampMs =
- AbsoluteTime.toStampMs(minCheckThreshold);
+ exchange.nextRefreshCheckStamp = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(minCheckThreshold),
+ );
await tx.exchanges.put(exchange);
});
return TaskRunResult.finished();
diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts
index 6ae021174..4e16d977d 100644
--- a/packages/taler-wallet-core/src/operations/reward.ts
+++ b/packages/taler-wallet-core/src/operations/reward.ts
@@ -50,6 +50,10 @@ import {
DenominationRecord,
RewardRecord,
RewardRecordStatus,
+ timestampPreciseFromDb,
+ timestampPreciseToDb,
+ timestampProtocolFromDb,
+ timestampProtocolToDb,
} from "../db.js";
import { makeErrorDetail } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
@@ -199,11 +203,11 @@ export async function prepareTip(
acceptedTimestamp: undefined,
status: RewardRecordStatus.DialogAccept,
rewardAmountRaw: Amounts.stringify(amount),
- rewardExpiration: tipPickupStatus.expiration,
+ rewardExpiration: timestampProtocolToDb(tipPickupStatus.expiration),
exchangeBaseUrl: tipPickupStatus.exchange_url,
next_url: tipPickupStatus.next_url,
merchantBaseUrl: res.merchantBaseUrl,
- createdTimestamp: TalerPreciseTimestamp.now(),
+ createdTimestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
merchantRewardId: res.merchantRewardId,
rewardAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
denomsSel: selectedDenoms,
@@ -229,7 +233,7 @@ export async function prepareTip(
rewardAmountRaw: Amounts.stringify(tipRecord.rewardAmountRaw),
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
merchantBaseUrl: tipRecord.merchantBaseUrl,
- expirationTimestamp: tipRecord.rewardExpiration,
+ expirationTimestamp: timestampProtocolFromDb(tipRecord.rewardExpiration),
rewardAmountEffective: Amounts.stringify(tipRecord.rewardAmountEffective),
walletRewardId: tipRecord.walletRewardId,
transactionId,
@@ -300,13 +304,16 @@ export async function processTip(
}
const tipStatusUrl = new URL(
- `tips/${tipRecord.merchantRewardId}/pickup`,
+ `rewards/${tipRecord.merchantRewardId}/pickup`,
tipRecord.merchantBaseUrl,
);
const req = { planchets: planchetsDetail };
logger.trace(`sending tip request: ${j2s(req)}`);
- const merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
+ const merchantResp = await ws.http.fetch(tipStatusUrl.href, {
+ method: "POST",
+ body: req,
+ });
logger.trace(`got tip response, status ${merchantResp.status}`);
@@ -411,7 +418,7 @@ export async function processTip(
return;
}
const oldTxState = computeRewardTransactionStatus(tr);
- tr.pickedUpTimestamp = TalerPreciseTimestamp.now();
+ tr.pickedUpTimestamp = timestampPreciseToDb(TalerPreciseTimestamp.now());
tr.status = RewardRecordStatus.Done;
await tx.rewards.put(tr);
const newTxState = computeRewardTransactionStatus(tr);
@@ -448,7 +455,9 @@ export async function acceptTip(
return { tipRecord };
}
const oldTxState = computeRewardTransactionStatus(tipRecord);
- tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now();
+ tipRecord.acceptedTimestamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
tipRecord.status = RewardRecordStatus.PendingPickup;
await tx.rewards.put(tipRecord);
const newTxState = computeRewardTransactionStatus(tipRecord);
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index f71d842c7..607d03470 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -23,12 +23,16 @@ import {
ConfirmPayResultType,
Duration,
IntegrationTestV2Args,
+ j2s,
Logger,
NotificationType,
+ RegisterAccountRequest,
stringToBytes,
+ TalerCorebankApiClient,
TestPayResult,
TransactionMajorState,
TransactionMinorState,
+ TransactionState,
TransactionType,
WithdrawTestBalanceRequest,
} from "@gnu-taler/taler-util";
@@ -73,16 +77,6 @@ import { getTransactionById, getTransactions } from "./transactions.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;
@@ -102,34 +96,27 @@ function makeId(length: number): string {
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;
+ const corebankApiBaseUrl = req.corebankApiBaseUrl;
logger.trace(
- `Registered bank user, bank access base url ${bankAccessApiBaseUrl}`,
+ `Registering bank user, bank access base url ${corebankApiBaseUrl}`,
);
- const bankUser = await registerRandomBankUser(ws.http, bankAccessApiBaseUrl);
+
+ const corebankClient = new TalerCorebankApiClient(corebankApiBaseUrl);
+
+ const bankUser = await corebankClient.createRandomBankUser();
logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
- const wresp = await createDemoBankWithdrawalUri(
- ws.http,
- bankAccessApiBaseUrl,
- bankUser,
+ corebankClient.setAuth(bankUser);
+
+ const wresp = await corebankClient.createWithdrawalOperation(
+ bankUser.username,
amount,
);
@@ -139,14 +126,14 @@ export async function withdrawTestBalance(
forcedDenomSel: req.forcedDenomSel,
});
- await confirmBankWithdrawalUri(
- ws.http,
- bankAccessApiBaseUrl,
- bankUser,
- wresp.withdrawal_id,
- );
+ await corebankClient.confirmWithdrawalOperation(bankUser.username, {
+ withdrawalOperationId: wresp.withdrawal_id,
+ });
}
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
function getMerchantAuthHeader(m: MerchantBackendInfo): Record<string, string> {
if (m.authToken) {
return {
@@ -157,80 +144,8 @@ function getMerchantAuthHeader(m: MerchantBackendInfo): Record<string, string> {
}
/**
- * Use the testing API of a demobank to create a taler://withdraw URI
- * that the wallet can then use to make a withdrawal.
+ * FIXME: User MerchantApiClient instead.
*/
-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,
@@ -258,6 +173,9 @@ async function refund(
return refundUri;
}
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
async function createOrder(
http: HttpRequestLibrary,
merchantBackend: MerchantBackendInfo,
@@ -287,6 +205,9 @@ async function createOrder(
return { orderId };
}
+/**
+ * FIXME: User MerchantApiClient instead.
+ */
async function checkPayment(
http: HttpRequestLibrary,
merchantBackend: MerchantBackendInfo,
@@ -300,16 +221,6 @@ async function checkPayment(
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,
@@ -376,7 +287,7 @@ export async function runIntegrationTest(
logger.info("withdrawing test balance");
await withdrawTestBalance(ws, {
amount: args.amountToWithdraw,
- bankAccessApiBaseUrl: args.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
exchangeBaseUrl: args.exchangeBaseUrl,
});
await waitUntilDone(ws);
@@ -404,7 +315,7 @@ export async function runIntegrationTest(
await withdrawTestBalance(ws, {
amount: Amounts.stringify(withdrawAmountTwo),
- bankAccessApiBaseUrl: args.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
exchangeBaseUrl: args.exchangeBaseUrl,
});
@@ -585,6 +496,49 @@ async function waitUntilPendingReady(
cancelNotifs();
}
+/**
+ * Wait until a transaction is in a particular state.
+ */
+export async function waitTransactionState(
+ ws: InternalWalletState,
+ transactionId: string,
+ txState: TransactionState,
+): Promise<void> {
+ logger.info(
+ `starting waiting for ${transactionId} to be in ${JSON.stringify(
+ txState,
+ )})`,
+ );
+ ws.ensureTaskLoopRunning();
+ let p: OpenedPromise<void> | undefined = undefined;
+ const cancelNotifs = ws.addNotificationListener((notif) => {
+ if (!p) {
+ return;
+ }
+ if (notif.type === NotificationType.TransactionStateTransition) {
+ p.resolve();
+ }
+ });
+ while (1) {
+ p = openPromise();
+ const tx = await getTransactionById(ws, {
+ transactionId,
+ });
+ if (
+ tx.txState.major === txState.major &&
+ tx.txState.minor === txState.minor
+ ) {
+ break;
+ }
+ // Wait until transaction state changed
+ await p.promise;
+ }
+ logger.info(
+ `done waiting for ${transactionId} to be in ${JSON.stringify(txState)}`,
+ );
+ cancelNotifs();
+}
+
export async function runIntegrationTest2(
ws: InternalWalletState,
args: IntegrationTestV2Args,
@@ -603,7 +557,7 @@ export async function runIntegrationTest2(
logger.info("withdrawing test balance");
await withdrawTestBalance(ws, {
amount: Amounts.stringify(amountToWithdraw),
- bankAccessApiBaseUrl: args.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
exchangeBaseUrl: args.exchangeBaseUrl,
});
await waitUntilDone(ws);
@@ -636,7 +590,7 @@ export async function runIntegrationTest2(
await withdrawTestBalance(ws, {
amount: Amounts.stringify(withdrawAmountTwo),
- bankAccessApiBaseUrl: args.bankAccessApiBaseUrl,
+ corebankApiBaseUrl: args.corebankApiBaseUrl,
exchangeBaseUrl: args.exchangeBaseUrl,
});
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index d7b277faf..cf2006406 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -20,6 +20,7 @@
import {
AbsoluteTime,
Amounts,
+ DepositTransactionTrackingState,
j2s,
Logger,
NotificationType,
@@ -65,7 +66,13 @@ import {
WithdrawalGroupStatus,
WithdrawalRecordType,
} from "../db.js";
-import { GetReadOnlyAccess, WalletStoresV1 } from "../index.js";
+import {
+ GetReadOnlyAccess,
+ timestampOptionalPreciseFromDb,
+ timestampPreciseFromDb,
+ timestampProtocolFromDb,
+ WalletStoresV1,
+} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
@@ -470,7 +477,7 @@ function buildTransactionForPushPaymentDebit(
expiration: contractTerms.purse_expiration,
summary: contractTerms.summary,
},
- timestamp: pi.timestampCreated,
+ timestamp: timestampPreciseFromDb(pi.timestampCreated),
talerUri: stringifyPayPushUri({
exchangeBaseUrl: pi.exchangeBaseUrl,
contractPriv: pi.contractPriv,
@@ -501,7 +508,7 @@ function buildTransactionForPullPaymentDebit(
expiration: contractTerms.purse_expiration,
summary: contractTerms.summary,
},
- timestamp: pi.timestampCreated,
+ timestamp: timestampPreciseFromDb(pi.timestampCreated),
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit,
peerPullDebitId: pi.peerPullDebitId,
@@ -543,8 +550,7 @@ function buildTransactionForPeerPullCredit(
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
amountRaw: Amounts.stringify(wsr.instructedAmount),
exchangeBaseUrl: wsr.exchangeBaseUrl,
- // Old transactions don't have it!
- timestamp: pullCredit.mergeTimestamp ?? TalerPreciseTimestamp.now(),
+ timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp),
info: {
expiration: peerContractTerms.purse_expiration,
summary: peerContractTerms.summary,
@@ -575,8 +581,7 @@ function buildTransactionForPeerPullCredit(
amountEffective: Amounts.stringify(pullCredit.estimatedAmountEffective),
amountRaw: Amounts.stringify(peerContractTerms.amount),
exchangeBaseUrl: pullCredit.exchangeBaseUrl,
- // Old transactions don't have it!
- timestamp: pullCredit.mergeTimestamp ?? TalerProtocolTimestamp.now(),
+ timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp),
info: {
expiration: peerContractTerms.purse_expiration,
summary: peerContractTerms.summary,
@@ -617,7 +622,7 @@ function buildTransactionForPeerPushCredit(
expiration: peerContractTerms.purse_expiration,
summary: peerContractTerms.summary,
},
- timestamp: wsr.timestampStart,
+ timestamp: timestampPreciseFromDb(wsr.timestampStart),
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId: pushInc.peerPushCreditId,
@@ -640,7 +645,7 @@ function buildTransactionForPeerPushCredit(
summary: peerContractTerms.summary,
},
kycUrl: pushInc.kycUrl,
- timestamp: pushInc.timestamp,
+ timestamp: timestampPreciseFromDb(pushInc.timestamp),
transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPushCredit,
peerPushCreditId: pushInc.peerPushCreditId,
@@ -673,7 +678,7 @@ function buildTransactionForBankIntegratedWithdraw(
},
kycUrl: wgRecord.kycUrl,
exchangeBaseUrl: wgRecord.exchangeBaseUrl,
- timestamp: wgRecord.timestampStart,
+ timestamp: timestampPreciseFromDb(wgRecord.timestampStart),
transactionId: constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId: wgRecord.withdrawalGroupId,
@@ -717,7 +722,7 @@ function buildTransactionForManualWithdraw(
},
kycUrl: withdrawalGroup.kycUrl,
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
- timestamp: withdrawalGroup.timestampStart,
+ timestamp: timestampPreciseFromDb(withdrawalGroup.timestampStart),
transactionId: constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
@@ -748,7 +753,7 @@ function buildTransactionForRefund(
tag: TransactionType.Payment,
proposalId: refundRecord.proposalId,
}),
- timestamp: refundRecord.timestampCreated,
+ timestamp: timestampPreciseFromDb(refundRecord.timestampCreated),
transactionId: constructTransactionIdentifier({
tag: TransactionType.Refund,
refundGroupId: refundRecord.refundGroupId,
@@ -786,7 +791,7 @@ function buildTransactionForRefresh(
refreshOutputAmount: Amounts.stringify(outputAmount),
originatingTransactionId:
refreshGroupRecord.reasonDetails?.originatingTransactionId,
- timestamp: refreshGroupRecord.timestampCreated,
+ timestamp: timestampPreciseFromDb(refreshGroupRecord.timestampCreated),
transactionId: constructTransactionIdentifier({
tag: TransactionType.Refresh,
refreshGroupId: refreshGroupRecord.refreshGroupId,
@@ -806,15 +811,26 @@ function buildTransactionForDeposit(
}
}
+ 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,
+ });
+ }
+
return {
type: TransactionType.Deposit,
txState: computeDepositTransactionStatus(dg),
txActions: computeDepositTransactionActions(dg),
amountRaw: Amounts.stringify(dg.counterpartyEffectiveDepositAmount),
amountEffective: Amounts.stringify(dg.totalPayCost),
- timestamp: dg.timestampCreated,
+ timestamp: timestampPreciseFromDb(dg.timestampCreated),
targetPaytoUri: dg.wire.payto_uri,
- wireTransferDeadline: dg.wireTransferDeadline,
+ wireTransferDeadline: timestampProtocolFromDb(dg.wireTransferDeadline),
transactionId: constructTransactionIdentifier({
tag: TransactionType.Deposit,
depositGroupId: dg.depositGroupId,
@@ -827,7 +843,7 @@ function buildTransactionForDeposit(
)) /
dg.statusPerCoin.length,
depositGroupId: dg.depositGroupId,
- trackingState: Object.values(dg.trackingState ?? {}),
+ trackingState,
deposited,
...(ort?.lastError ? { error: ort.lastError } : {}),
};
@@ -845,7 +861,7 @@ function buildTransactionForTip(
txActions: computeTipTransactionActions(tipRecord),
amountEffective: Amounts.stringify(tipRecord.rewardAmountEffective),
amountRaw: Amounts.stringify(tipRecord.rewardAmountRaw),
- timestamp: tipRecord.acceptedTimestamp,
+ timestamp: timestampPreciseFromDb(tipRecord.acceptedTimestamp),
transactionId: constructTransactionIdentifier({
tag: TransactionType.Reward,
walletRewardId: tipRecord.walletRewardId,
@@ -922,7 +938,7 @@ async function buildTransactionForPurchase(
: Amounts.stringify(purchaseRecord.refundAmountAwaiting),
refunds,
posConfirmation: purchaseRecord.posConfirmation,
- timestamp,
+ timestamp: timestampPreciseFromDb(timestamp),
transactionId: constructTransactionIdentifier({
tag: TransactionType.Payment,
proposalId: purchaseRecord.proposalId,
diff --git a/packages/taler-wallet-core/src/operations/withdraw.test.ts b/packages/taler-wallet-core/src/operations/withdraw.test.ts
index 2d9286610..cb8aa5e81 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.test.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.test.ts
@@ -16,7 +16,11 @@
import { Amounts, DenomKeyType } from "@gnu-taler/taler-util";
import test from "ava";
-import { DenominationRecord, DenominationVerificationStatus } from "../db.js";
+import {
+ DenominationRecord,
+ DenominationVerificationStatus,
+ timestampProtocolToDb,
+} from "../db.js";
import { selectWithdrawalDenominations } from "../util/coinSelection.js";
test("withdrawal selection bug repro", (t) => {
@@ -64,22 +68,22 @@ 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",
value: "KUDOS:1000",
- listIssueDate: { t_s: 0 },
+ listIssueDate: timestampProtocolToDb({ t_s: 0 }),
},
{
denomPub: {
@@ -119,22 +123,22 @@ 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,
value: "KUDOS:10",
currency: "KUDOS",
- listIssueDate: { t_s: 0 },
+ listIssueDate: timestampProtocolToDb({ t_s: 0 }),
},
{
denomPub: {
@@ -173,22 +177,22 @@ 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,
value: "KUDOS:5",
currency: "KUDOS",
- listIssueDate: { t_s: 0 },
+ listIssueDate: timestampProtocolToDb({ t_s: 0 }),
},
{
denomPub: {
@@ -228,22 +232,22 @@ 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,
value: "KUDOS:1",
currency: "KUDOS",
- listIssueDate: { t_s: 0 },
+ listIssueDate: timestampProtocolToDb({ t_s: 0 }),
},
{
denomPub: {
@@ -282,18 +286,18 @@ 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,
value: Amounts.stringify({
currency: "KUDOS",
@@ -301,7 +305,7 @@ test("withdrawal selection bug repro", (t) => {
value: 0,
}),
currency: "KUDOS",
- listIssueDate: { t_s: 0 },
+ listIssueDate: timestampProtocolToDb({ t_s: 0 }),
},
{
denomPub: {
@@ -340,22 +344,22 @@ 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,
value: "KUDOS:2",
currency: "KUDOS",
- listIssueDate: { t_s: 0 },
+ listIssueDate: timestampProtocolToDb({ t_s: 0 }),
},
];
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index bae348dc1..eff427bec 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -135,6 +135,7 @@ import {
ExchangeEntryDbUpdateStatus,
PendingTaskType,
isWithdrawableDenom,
+ timestampPreciseToDb,
} from "../index.js";
import {
TransitionInfo,
@@ -573,7 +574,7 @@ export async function getBankWithdrawalInfo(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE,
{
- exchangeProtocolVersion: config.version,
+ bankProtocolVersion: config.version,
walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
},
"bank integration protocol version not compatible with wallet",
@@ -816,10 +817,10 @@ async function handleKycRequired(
amlStatus === AmlStatus.normal || amlStatus === undefined
? WithdrawalGroupStatus.PendingKyc
: amlStatus === AmlStatus.pending
- ? WithdrawalGroupStatus.PendingAml
- : amlStatus === AmlStatus.fronzen
- ? WithdrawalGroupStatus.SuspendedAml
- : assertUnreachable(amlStatus);
+ ? WithdrawalGroupStatus.PendingAml
+ : amlStatus === AmlStatus.fronzen
+ ? WithdrawalGroupStatus.SuspendedAml
+ : assertUnreachable(amlStatus);
await tx.withdrawalGroups.put(wg2);
const newTxState = computeWithdrawalTransactionStatus(wg2);
@@ -1149,8 +1150,7 @@ export async function updateWithdrawalDenoms(
denom.verificationStatus === DenominationVerificationStatus.Unverified
) {
logger.trace(
- `Validating denomination (${current + 1}/${
- denominations.length
+ `Validating denomination (${current + 1}/${denominations.length
}) signature of ${denom.denomPubHash}`,
);
let valid = false;
@@ -1244,7 +1244,7 @@ async function queryReserve(
if (
resp.status === 404 &&
result.talerErrorResponse.code ===
- TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
+ TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
) {
return { ready: false };
} else {
@@ -1330,7 +1330,7 @@ async function processWithdrawalGroupAbortingBank(
}
const txStatusOld = computeWithdrawalTransactionStatus(wg);
wg.status = WithdrawalGroupStatus.AbortedBank;
- wg.timestampFinish = TalerPreciseTimestamp.now();
+ wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
const txStatusNew = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg);
return {
@@ -1463,7 +1463,7 @@ async function processWithdrawalGroupPendingReady(
}
const txStatusOld = computeWithdrawalTransactionStatus(wg);
wg.status = WithdrawalGroupStatus.Done;
- wg.timestampFinish = TalerPreciseTimestamp.now();
+ wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
const txStatusNew = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg);
return {
@@ -1559,7 +1559,7 @@ async function processWithdrawalGroupPendingReady(
const oldTxState = computeWithdrawalTransactionStatus(wg);
logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
- wg.timestampFinish = TalerPreciseTimestamp.now();
+ wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
wg.status = WithdrawalGroupStatus.Done;
await makeCoinsVisible(ws, tx, transactionId);
}
@@ -1779,7 +1779,7 @@ export async function getExchangeWithdrawalInfo(
) {
logger.warn(
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
- `(exchange has ${exchangeDetails.protocolVersionRange}), checking for updates`,
+ `(exchange has ${exchangeDetails.protocolVersionRange}), checking for updates`,
);
}
}
@@ -2052,8 +2052,9 @@ async function registerReserveWithBank(
if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
throw Error("invariant failed");
}
- r.wgInfo.bankInfo.timestampReserveInfoPosted =
- AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
+ r.wgInfo.bankInfo.timestampReserveInfoPosted = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now()),
+ );
const oldTxState = computeWithdrawalTransactionStatus(r);
r.status = WithdrawalGroupStatus.PendingWaitConfirmBank;
const newTxState = computeWithdrawalTransactionStatus(r);
@@ -2135,7 +2136,7 @@ async function processReserveBankStatus(
}
const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
const oldTxState = computeWithdrawalTransactionStatus(r);
- r.wgInfo.bankInfo.timestampBankConfirmed = now;
+ r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now);
r.status = WithdrawalGroupStatus.FailedBankAborted;
const newTxState = computeWithdrawalTransactionStatus(r);
await tx.withdrawalGroups.put(r);
@@ -2184,7 +2185,7 @@ async function processReserveBankStatus(
if (status.transfer_done) {
logger.info("withdrawal: transfer confirmed by bank.");
const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
- r.wgInfo.bankInfo.timestampBankConfirmed = now;
+ r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now);
r.status = WithdrawalGroupStatus.PendingQueryingStatus;
} else {
logger.info("withdrawal: transfer not yet confirmed by bank");
@@ -2290,7 +2291,7 @@ export async function internalPrepareCreateWithdrawalGroup(
denomsSel: initialDenomSel,
exchangeBaseUrl: canonExchange,
instructedAmount: Amounts.stringify(amount),
- timestampStart: now,
+ timestampStart: timestampPreciseToDb(now),
rawWithdrawalAmount: initialDenomSel.totalWithdrawCost,
effectiveWithdrawalAmount: initialDenomSel.totalCoinValue,
secretSeed,
@@ -2344,8 +2345,7 @@ export async function internalPerformCreateWithdrawalGroup(
if (!prep.creationInfo) {
return { withdrawalGroup, transitionInfo: undefined };
}
- const { amount, canonExchange, exchangeDetails } =
- prep.creationInfo;
+ const { amount, canonExchange, exchangeDetails } = prep.creationInfo;
await tx.withdrawalGroups.add(withdrawalGroup);
await tx.reserves.put({
@@ -2355,7 +2355,7 @@ export async function internalPerformCreateWithdrawalGroup(
const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
if (exchange) {
- exchange.lastWithdrawal = TalerPreciseTimestamp.now();
+ exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now());
exchange.entryStatus = ExchangeEntryDbRecordStatus.Used;
await tx.exchanges.put(exchange);
}
@@ -2546,11 +2546,7 @@ export async function createManualWithdrawal(
});
const exchangePaytoUris = await ws.db
- .mktx((x) => [
- x.withdrawalGroups,
- x.exchanges,
- x.exchangeDetails,
- ])
+ .mktx((x) => [x.withdrawalGroups, x.exchanges, x.exchangeDetails])
.runReadOnly(async (tx) => {
return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId);
});
diff --git a/packages/taler-wallet-core/src/pending-types.ts b/packages/taler-wallet-core/src/pending-types.ts
index 627888b4d..e7a40e81b 100644
--- a/packages/taler-wallet-core/src/pending-types.ts
+++ b/packages/taler-wallet-core/src/pending-types.ts
@@ -25,7 +25,7 @@
* Imports.
*/
import { TalerErrorDetail, AbsoluteTime } from "@gnu-taler/taler-util";
-import { RetryInfo } from "./operations/common.js";
+import { DbRetryInfo } from "./operations/common.js";
export enum PendingTaskType {
ExchangeUpdate = "exchange-update",
@@ -137,7 +137,7 @@ export interface PendingRefreshTask {
lastError?: TalerErrorDetail;
refreshGroupId: string;
finishedPerCoin: boolean[];
- retryInfo?: RetryInfo;
+ retryInfo?: DbRetryInfo;
}
/**
@@ -156,7 +156,7 @@ export interface PendingTipPickupTask {
export interface PendingPurchaseTask {
type: PendingTaskType.Purchase;
proposalId: string;
- retryInfo?: RetryInfo;
+ retryInfo?: DbRetryInfo;
/**
* Status of the payment as string, used only for debugging.
*/
@@ -167,7 +167,7 @@ export interface PendingPurchaseTask {
export interface PendingRecoupTask {
type: PendingTaskType.Recoup;
recoupGroupId: string;
- retryInfo?: RetryInfo;
+ retryInfo?: DbRetryInfo;
lastError: TalerErrorDetail | undefined;
}
@@ -177,7 +177,7 @@ export interface PendingRecoupTask {
export interface PendingWithdrawTask {
type: PendingTaskType.Withdraw;
lastError: TalerErrorDetail | undefined;
- retryInfo?: RetryInfo;
+ retryInfo?: DbRetryInfo;
withdrawalGroupId: string;
}
@@ -187,7 +187,7 @@ export interface PendingWithdrawTask {
export interface PendingDepositTask {
type: PendingTaskType.Deposit;
lastError: TalerErrorDetail | undefined;
- retryInfo: RetryInfo | undefined;
+ retryInfo: DbRetryInfo | undefined;
depositGroupId: string;
}
@@ -233,7 +233,7 @@ export interface PendingTaskInfoCommon {
* Retry info. Currently used to stop the wallet after any operation
* exceeds a number of retries.
*/
- retryInfo?: RetryInfo;
+ retryInfo?: DbRetryInfo;
}
/**
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts
index 2a322c4a9..81a656f8a 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -15,65 +15,58 @@
*/
import {
AbsoluteTime,
- AgeRestriction,
- AmountJson,
AmountString,
Amounts,
DenomKeyType,
Duration,
- TransactionAmountMode,
+ j2s,
} from "@gnu-taler/taler-util";
import test, { ExecutionContext } from "ava";
-import { AvailableDenom, testing_greedySelectPeer } from "./coinSelection.js"
-
-type Tester<T> = {
- deep: {
- equal(another: T): ReturnType<ExecutionContext["deepEqual"]>;
- equals(another: T): ReturnType<ExecutionContext["deepEqual"]>;
- }
-}
-
-function expect<T>(t: ExecutionContext, thing: T): Tester<T> {
- return {
- deep: {
- equal: (another: T) => t.deepEqual(thing, another),
- equals: (another: T) => t.deepEqual(thing, another),
- },
- };
-}
+import {
+ AvailableDenom,
+ testing_greedySelectPeer,
+ testing_selectGreedy,
+} from "./coinSelection.js";
const inTheDistantFuture = AbsoluteTime.toProtocolTimestamp(
- AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ hours: 1 }))
-)
+ AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ hours: 1 })),
+);
const inThePast = AbsoluteTime.toProtocolTimestamp(
- AbsoluteTime.subtractDuraction(AbsoluteTime.now(), Duration.fromSpec({ hours: 1 }))
-)
-
-test("should select the coin", (t) => {
- const instructedAmount = Amounts.parseOrThrow("LOCAL:2")
+ AbsoluteTime.subtractDuraction(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+);
+
+test("p2p: should select the coin", (t) => {
+ const instructedAmount = Amounts.parseOrThrow("LOCAL:2");
const tally = {
amountAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency),
};
const coins = testing_greedySelectPeer(
- createCandidates([{
- amount: "LOCAL:10",
- numAvailable: 5,
- depositFee: "LOCAL:0.1",
- fromExchange: "http://exchange.localhost/",
- }]),
- instructedAmount,
- tally
+ createCandidates([
+ {
+ amount: "LOCAL:10",
+ numAvailable: 5,
+ depositFee: "LOCAL:0.1",
+ fromExchange: "http://exchange.localhost/",
+ },
+ ]),
+ instructedAmount,
+ tally,
);
+ t.log(j2s(coins));
+
expect(t, coins).deep.equal({
"hash0;32;http://exchange.localhost/": {
exchangeBaseUrl: "http://exchange.localhost/",
denomPubHash: "hash0",
maxAge: 32,
- contributions: [Amounts.parseOrThrow("LOCAL:2")],
- }
+ contributions: [Amounts.parseOrThrow("LOCAL:2.1")],
+ },
});
expect(t, tally).deep.equal({
@@ -81,25 +74,26 @@ test("should select the coin", (t) => {
depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.1"),
lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"),
});
-
});
-test("should select 3 coins", (t) => {
- const instructedAmount = Amounts.parseOrThrow("LOCAL:20")
+test("p2p: should select 3 coins", (t) => {
+ const instructedAmount = Amounts.parseOrThrow("LOCAL:20");
const tally = {
amountAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency),
};
const coins = testing_greedySelectPeer(
- createCandidates([{
- amount: "LOCAL:10",
- numAvailable: 5,
- depositFee: "LOCAL:0.1",
- fromExchange: "http://exchange.localhost/",
- }]),
- instructedAmount,
- tally
+ createCandidates([
+ {
+ amount: "LOCAL:10",
+ numAvailable: 5,
+ depositFee: "LOCAL:0.1",
+ fromExchange: "http://exchange.localhost/",
+ },
+ ]),
+ instructedAmount,
+ tally,
);
expect(t, coins).deep.equal({
@@ -110,9 +104,9 @@ test("should select 3 coins", (t) => {
contributions: [
Amounts.parseOrThrow("LOCAL:9.9"),
Amounts.parseOrThrow("LOCAL:9.9"),
- Amounts.parseOrThrow("LOCAL:0.2")
+ Amounts.parseOrThrow("LOCAL:0.5"),
],
- }
+ },
});
expect(t, tally).deep.equal({
@@ -120,62 +114,142 @@ test("should select 3 coins", (t) => {
depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.3"),
lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"),
});
-
});
-test("can't select since the instructed amount is too high", (t) => {
- const instructedAmount = Amounts.parseOrThrow("LOCAL:60")
+test("p2p: can't select since the instructed amount is too high", (t) => {
+ const instructedAmount = Amounts.parseOrThrow("LOCAL:60");
const tally = {
amountAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency),
};
const coins = testing_greedySelectPeer(
- createCandidates([{
- amount: "LOCAL:10",
- numAvailable: 5,
- depositFee: "LOCAL:0.1",
- fromExchange: "http://exchange.localhost/",
- }]),
- instructedAmount,
- tally
+ createCandidates([
+ {
+ amount: "LOCAL:10",
+ numAvailable: 5,
+ depositFee: "LOCAL:0.1",
+ fromExchange: "http://exchange.localhost/",
+ },
+ ]),
+ instructedAmount,
+ tally,
);
expect(t, coins).deep.equal(undefined);
expect(t, tally).deep.equal({
- amountAcc: Amounts.parseOrThrow("LOCAL:49.5"),
+ amountAcc: Amounts.parseOrThrow("LOCAL:49"),
depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.5"),
lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"),
});
-
});
+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,
+ amountWireFeeLimitRemaining: zero,
+ amountDepositFeeLimitRemaining: zero,
+ customerDepositFees: zero,
+ customerWireFees: zero,
+ wireFeeCoveredForExchange: new Set<string>(),
+ lastDepositFee: zero,
+ };
+ const coins = testing_selectGreedy(
+ {
+ auditors: [],
+ exchanges: [
+ {
+ exchangeBaseUrl: "http://exchange.localhost/",
+ exchangePub: "E5M8CGRDHXF1RCVP3B8TQCTDYNQ7T4XHWR5SVEQRGVVMVME41VJ0",
+ },
+ ],
+ contractTermsAmount: payment,
+ depositFeeLimit: zero,
+ wireFeeAmortization: 1,
+ wireFeeLimit: zero,
+ prevPayCoins: [],
+ wireMethod: "x-taler-bank",
+ },
+ createCandidates([
+ {
+ amount: "LOCAL:10",
+ numAvailable: 5,
+ depositFee: "LOCAL:0.1",
+ fromExchange: "http://exchange.localhost/",
+ },
+ ]),
+ { "http://exchange.localhost/": exchangeWireFee },
+ tally,
+ );
+ expect(t, coins).deep.equal({
+ "hash0;32;http://exchange.localhost/": {
+ exchangeBaseUrl: "http://exchange.localhost/",
+ denomPubHash: "hash0",
+ maxAge: 32,
+ contributions: [Amounts.parseOrThrow("LOCAL:2.2")],
+ },
+ });
+ expect(t, tally).deep.equal({
+ amountPayRemaining: Amounts.parseOrThrow("LOCAL:2"),
+ amountWireFeeLimitRemaining: zero,
+ amountDepositFeeLimitRemaining: zero,
+ customerDepositFees: zero,
+ customerWireFees: zero,
+ wireFeeCoveredForExchange: new Set(),
+ lastDepositFee: zero,
+ });
+});
-function createCandidates(ar: {amount: AmountString, depositFee: AmountString, numAvailable: number, fromExchange: string}[]): AvailableDenom[] {
- return ar.map((r,idx) => {
+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"
+ denomPub: {
+ age_mask: 0,
+ cipher: DenomKeyType.Rsa,
+ rsa_public_key: "PPP",
},
- "denomPubHash": `hash${idx}`,
- "value": r.amount,
- "feeDeposit": r.depositFee,
- "feeRefresh": "LOCAL:0",
- "feeRefund": "LOCAL:0",
- "feeWithdraw": "LOCAL:0",
- "stampExpireDeposit": inTheDistantFuture,
- "stampExpireLegal": inTheDistantFuture,
- "stampExpireWithdraw": inTheDistantFuture,
- "stampStart": inThePast,
- "exchangeBaseUrl": r.fromExchange,
- "numAvailable": r.numAvailable,
- "maxAge": 32,
-
- }
- })
+ denomPubHash: `hash${idx}`,
+ value: r.amount,
+ feeDeposit: r.depositFee,
+ feeRefresh: "LOCAL:0",
+ feeRefund: "LOCAL:0",
+ feeWithdraw: "LOCAL:0",
+ stampExpireDeposit: inTheDistantFuture,
+ stampExpireLegal: inTheDistantFuture,
+ stampExpireWithdraw: inTheDistantFuture,
+ stampStart: inThePast,
+ exchangeBaseUrl: r.fromExchange,
+ numAvailable: r.numAvailable,
+ maxAge: 32,
+ };
+ });
+}
+
+type Tester<T> = {
+ deep: {
+ equal(another: T): ReturnType<ExecutionContext["deepEqual"]>;
+ equals(another: T): ReturnType<ExecutionContext["deepEqual"]>;
+ };
+};
+
+function expect<T>(t: ExecutionContext, thing: T): Tester<T> {
+ return {
+ deep: {
+ equal: (another: T) => t.deepEqual(thing, another),
+ equals: (another: T) => t.deepEqual(thing, another),
+ },
+ };
}
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
index 0885215dd..8c90f26f1 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -419,6 +419,11 @@ interface SelResult {
};
}
+export function testing_selectGreedy(
+ ...args: Parameters<typeof selectGreedy>
+): ReturnType<typeof selectGreedy> {
+ return selectGreedy(...args);
+}
function selectGreedy(
req: SelectPayCoinRequestNg,
candidateDenoms: AvailableDenom[],
@@ -897,9 +902,12 @@ interface PeerCoinSelectionTally {
/**
* exporting for testing
*/
-export function testing_greedySelectPeer(...args: Parameters<typeof greedySelectPeer>): ReturnType<typeof greedySelectPeer> {
- return greedySelectPeer(...args)
+export function testing_greedySelectPeer(
+ ...args: Parameters<typeof greedySelectPeer>
+): ReturnType<typeof greedySelectPeer> {
+ return greedySelectPeer(...args);
}
+
function greedySelectPeer(
candidates: AvailableDenom[],
instructedAmount: AmountLike,
@@ -918,11 +926,16 @@ function greedySelectPeer(
instructedAmount,
tally.amountAcc,
).amount;
- const coinContrib = Amounts.sub(denom.value, denom.feeDeposit).amount
+ // Maximum amount the coin could effectively contribute.
+ const maxCoinContrib = Amounts.sub(denom.value, denom.feeDeposit).amount;
+
+ const coinSpend = Amounts.min(
+ Amounts.add(amountPayRemaining, denom.feeDeposit).amount,
+ maxCoinContrib,
+ );
- const coinSpend = Amounts.min(amountPayRemaining, coinContrib)
-
tally.amountAcc = Amounts.add(tally.amountAcc, coinSpend).amount;
+ tally.amountAcc = Amounts.sub(tally.amountAcc, denom.feeDeposit).amount;
tally.depositFeesAcc = Amounts.add(
tally.depositFeesAcc,
@@ -930,7 +943,7 @@ function greedySelectPeer(
).amount;
tally.lastDepositFee = Amounts.parseOrThrow(denom.feeDeposit);
-
+
contributions.push(coinSpend);
}
if (contributions.length > 0) {
diff --git a/packages/taler-wallet-core/src/util/denominations.ts b/packages/taler-wallet-core/src/util/denominations.ts
index 76716cf7a..db6e69956 100644
--- a/packages/taler-wallet-core/src/util/denominations.ts
+++ b/packages/taler-wallet-core/src/util/denominations.ts
@@ -26,10 +26,9 @@ import {
FeeDescriptionPair,
TalerProtocolTimestamp,
TimePoint,
- WireFee,
} from "@gnu-taler/taler-util";
import { DenominationRecord } from "../db.js";
-import { WalletConfig } from "../index.js";
+import { timestampProtocolFromDb } from "../index.js";
/**
* Given a list of denominations with the same value and same period of time:
@@ -457,9 +456,11 @@ export function isWithdrawableDenom(
denomselAllowLate?: boolean,
): boolean {
const now = AbsoluteTime.now();
- const start = AbsoluteTime.fromProtocolTimestamp(d.stampStart);
+ const start = AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(d.stampStart),
+ );
const withdrawExpire = AbsoluteTime.fromProtocolTimestamp(
- d.stampExpireWithdraw,
+ timestampProtocolFromDb(d.stampExpireWithdraw),
);
const started = AbsoluteTime.cmp(now, start) >= 0;
let lastPossibleWithdraw: AbsoluteTime;
diff --git a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts
index 54c08eee4..a0394a687 100644
--- a/packages/taler-wallet-core/src/util/instructedAmountConversion.ts
+++ b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts
@@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { GlobalIDB } from "@gnu-taler/idb-bridge";
import {
AbsoluteTime,
AgeRestriction,
@@ -29,14 +30,14 @@ import {
parsePaytoUri,
strcmp,
} from "@gnu-taler/taler-util";
-import { checkDbInvariant } from "./invariants.js";
import {
DenominationRecord,
InternalWalletState,
getExchangeDetails,
+ timestampProtocolFromDb,
} from "../index.js";
import { CoinInfo } from "./coinSelection.js";
-import { GlobalIDB } from "@gnu-taler/idb-bridge";
+import { checkDbInvariant } from "./invariants.js";
/**
* If the operation going to be plan subtracts
@@ -224,10 +225,10 @@ async function getAvailableDenoms(
);
for (const denom of ds) {
const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
- denom.stampExpireWithdraw,
+ timestampProtocolFromDb(denom.stampExpireWithdraw),
);
const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
- denom.stampExpireDeposit,
+ timestampProtocolFromDb(denom.stampExpireDeposit),
);
creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
@@ -270,10 +271,10 @@ async function getAvailableDenoms(
continue;
}
const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
- denom.stampExpireWithdraw,
+ timestampProtocolFromDb(denom.stampExpireWithdraw),
);
const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
- denom.stampExpireDeposit,
+ timestampProtocolFromDb(denom.stampExpireDeposit),
);
creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
@@ -318,7 +319,9 @@ function buildCoinInfoFromDenom(
exchangeBaseUrl: denom.exchangeBaseUrl,
duration: AbsoluteTime.difference(
AbsoluteTime.now(),
- AbsoluteTime.fromProtocolTimestamp(denom.stampExpireDeposit),
+ AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(denom.stampExpireDeposit),
+ ),
),
totalAvailable: total,
value: Amounts.parseOrThrow(denom.value),
diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts
index 8b9177bc3..022f4900d 100644
--- a/packages/taler-wallet-core/src/versions.ts
+++ b/packages/taler-wallet-core/src/versions.ts
@@ -29,7 +29,7 @@ export const WALLET_EXCHANGE_PROTOCOL_VERSION = "17:0:0";
export const WALLET_MERCHANT_PROTOCOL_VERSION = "2:0:1";
/**
- * Protocol version spoken with the merchant.
+ * Protocol version spoken with the bank.
*
* Uses libtool's current:revision:age versioning.
*/
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 67c05a42f..375e0a1b2 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -104,11 +104,13 @@ import {
TestPayArgs,
TestPayResult,
TestingSetTimetravelRequest,
+ TestingWaitTransactionRequest,
Transaction,
TransactionByIdRequest,
TransactionsRequest,
TransactionsResponse,
TxIdResponse,
+ UpdateExchangeEntryRequest,
UserAttentionByIdRequest,
UserAttentionsCountResponse,
UserAttentionsRequest,
@@ -118,7 +120,6 @@ import {
WalletContractData,
WalletCoreVersion,
WalletCurrencyInfo,
- WithdrawFakebankRequest,
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
} from "@gnu-taler/taler-util";
@@ -199,7 +200,6 @@ export enum WalletApiOperation {
GenerateDepositGroupTxId = "generateDepositGroupTxId",
CreateDepositGroup = "createDepositGroup",
SetWalletDeviceId = "setWalletDeviceId",
- WithdrawFakebank = "withdrawFakebank",
ImportDb = "importDb",
ExportDb = "exportDb",
PreparePeerPushCredit = "preparePeerPushCredit",
@@ -216,12 +216,14 @@ export enum WalletApiOperation {
ValidateIban = "validateIban",
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
+ TestingWaitTransactionState = "testingWaitTransactionState",
TestingSetTimetravel = "testingSetTimetravel",
GetScopedCurrencyInfo = "getScopedCurrencyInfo",
ListStoredBackups = "listStoredBackups",
CreateStoredBackup = "createStoredBackup",
DeleteStoredBackup = "deleteStoredBackup",
RecoverStoredBackup = "recoverStoredBackup",
+ UpdateExchangeEntry = "updateExchangeEntry",
}
// group: Initialization
@@ -252,6 +254,11 @@ export type GetVersionOp = {
*/
export type WalletConfigParameter = RecursivePartial<WalletConfig>;
+export interface BuiltinExchange {
+ exchangeBaseUrl: string;
+ currencyHint?: string;
+}
+
export interface WalletConfig {
/**
* Initialization values useful for a complete startup.
@@ -259,7 +266,7 @@ export interface WalletConfig {
* These are values may be overridden by different wallets
*/
builtin: {
- exchanges: string[];
+ exchanges: BuiltinExchange[];
};
/**
@@ -557,6 +564,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;
@@ -935,17 +951,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 = {
@@ -1018,6 +1023,15 @@ export type TestingWaitRefreshesFinal = {
};
/**
+ * Wait until a transaction is in a particular state.
+ */
+export type TestingWaitTransactionStateOp = {
+ op: WalletApiOperation.TestingWaitTransactionState;
+ request: TestingWaitTransactionRequest;
+ response: EmptyObject;
+};
+
+/**
* Set a coin as (un-)suspended.
* Suspended coins won't be used for payments.
*/
@@ -1040,7 +1054,6 @@ export type ForceRefreshOp = {
export type WalletOperations = {
[WalletApiOperation.InitWallet]: InitWalletOp;
[WalletApiOperation.GetVersion]: GetVersionOp;
- [WalletApiOperation.WithdrawFakebank]: WithdrawFakebankOp;
[WalletApiOperation.PreparePayForUri]: PreparePayForUriOp;
[WalletApiOperation.SharePayment]: SharePaymentOp;
[WalletApiOperation.PreparePayForTemplate]: PreparePayForTemplateOp;
@@ -1122,11 +1135,13 @@ export type WalletOperations = {
[WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal;
[WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal;
[WalletApiOperation.TestingSetTimetravel]: TestingSetTimetravelOp;
+ [WalletApiOperation.TestingWaitTransactionState]: TestingWaitTransactionStateOp;
[WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp;
[WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp;
[WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp;
[WalletApiOperation.DeleteStoredBackup]: DeleteStoredBackupOp;
[WalletApiOperation.RecoverStoredBackup]: RecoverStoredBackupsOp;
+ [WalletApiOperation.UpdateExchangeEntry]: UpdateExchangeEntryOp;
};
export type WalletCoreRequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 2d0878afc..ead0ee407 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -127,6 +127,8 @@ import {
codecForRecoverStoredBackupRequest,
codecForTestingSetTimetravelRequest,
setDangerousTimetravel,
+ TestingWaitTransactionRequest,
+ codecForUpdateExchangeEntryRequest,
} from "@gnu-taler/taler-util";
import type { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
@@ -198,7 +200,6 @@ import {
downloadTosFromAcceptedFormat,
getExchangeDetails,
getExchangeRequestTimeout,
- provideExchangeRecordInTx,
updateExchangeFromUrl,
updateExchangeFromUrlHandler,
} from "./operations/exchanges.js";
@@ -250,6 +251,7 @@ import {
runIntegrationTest,
runIntegrationTest2,
testPay,
+ waitTransactionState,
waitUntilDone,
waitUntilRefreshesDone,
withdrawTestBalance,
@@ -532,10 +534,12 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
logger.trace("defaults already applied");
return;
}
- for (const baseUrl of ws.config.builtin.exchanges) {
- await addPresetExchangeEntry(tx, baseUrl);
- const now = AbsoluteTime.now();
- provideExchangeRecordInTx(ws, tx, baseUrl, now);
+ for (const exch of ws.config.builtin.exchanges) {
+ await addPresetExchangeEntry(
+ tx,
+ exch.exchangeBaseUrl,
+ exch.currencyHint,
+ );
}
await tx.config.put({
key: ConfigRecordKey.CurrencyDefaultsApplied,
@@ -923,9 +927,9 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
ageCommitmentProof: c.ageCommitmentProof,
spend_allocation: c.spendAllocation
? {
- amount: c.spendAllocation.amount,
- id: c.spendAllocation.id,
- }
+ amount: c.spendAllocation.amount,
+ id: c.spendAllocation.id,
+ }
: undefined,
});
}
@@ -1069,8 +1073,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
case WalletApiOperation.WithdrawTestkudos: {
await withdrawTestBalance(ws, {
amount: "TESTKUDOS:10",
- bankAccessApiBaseUrl:
- "https://bank.test.taler.net/demobanks/default/access-api/",
+ corebankApiBaseUrl: "https://bank.test.taler.net/",
exchangeBaseUrl: "https://exchange.test.taler.net/",
});
return {
@@ -1120,6 +1123,11 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
});
return {};
}
+ case WalletApiOperation.UpdateExchangeEntry: {
+ const req = codecForUpdateExchangeEntryRequest().decode(payload);
+ await updateExchangeFromUrl(ws, req.exchangeBaseUrl, {});
+ return {};
+ }
case WalletApiOperation.ListExchanges: {
return await getExchanges(ws);
}
@@ -1261,7 +1269,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
`templates/${url.templateId}`,
url.merchantBaseUrl,
);
- const httpReq = await ws.http.postJson(reqUrl.href, templateDetails);
+ const httpReq = await ws.http.fetch(reqUrl.href, {
+ method: "POST",
+ body: templateDetails,
+ });
const resp = await readSuccessResponseJsonOrThrow(
httpReq,
codecForMerchantPostOrderResponse(),
@@ -1411,6 +1422,11 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const resp = await getBackupRecovery(ws);
return resp;
}
+ case WalletApiOperation.TestingWaitTransactionState: {
+ const req = payload as TestingWaitTransactionRequest;
+ await waitTransactionState(ws, req.transactionId, req.txState);
+ return {};
+ }
case WalletApiOperation.GetScopedCurrencyInfo: {
// Ignore result, just validate in this mock implementation
codecForGetCurrencyInfoRequest().decode(payload);
@@ -1489,40 +1505,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
trustedExchanges: [],
};
}
- case WalletApiOperation.WithdrawFakebank: {
- const req = codecForWithdrawFakebankRequest().decode(payload);
- const amount = Amounts.parseOrThrow(req.amount);
- const details = await getExchangeWithdrawalInfo(
- ws,
- req.exchange,
- amount,
- undefined,
- );
- 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?receiver-name=Foo",
- },
- );
- 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" });
}
@@ -1691,7 +1673,12 @@ export class Wallet {
public static defaultConfig: Readonly<WalletConfig> = {
builtin: {
- exchanges: ["https://exchange.demo.taler.net/"],
+ exchanges: [
+ {
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ currencyHint: "KUDOS",
+ },
+ ],
},
features: {
allowHttp: false,
diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json
index 35a4fcffe..442a1f962 100644
--- a/packages/taler-wallet-embedded/package.json
+++ b/packages/taler-wallet-embedded/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-embedded",
- "version": "0.9.3-dev.17",
+ "version": "0.9.3-dev.27",
"description": "",
"engines": {
"node": ">=0.18.0"
@@ -39,4 +39,4 @@
"@gnu-taler/anastasis-core": "workspace:*",
"tslib": "^2.5.3"
}
-}
+} \ No newline at end of file
diff --git a/packages/taler-wallet-embedded/src/wallet-qjs.ts b/packages/taler-wallet-embedded/src/wallet-qjs.ts
index 5e2f1e0a4..1b3e3ae81 100644
--- a/packages/taler-wallet-embedded/src/wallet-qjs.ts
+++ b/packages/taler-wallet-embedded/src/wallet-qjs.ts
@@ -278,8 +278,8 @@ export async function testWithGv() {
await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
amountToSpend: "KUDOS:1",
amountToWithdraw: "KUDOS:3",
- bankAccessApiBaseUrl:
- "https://bank.demo.taler.net/demobanks/default/access-api/",
+ corebankApiBaseUrl:
+ "https://bank.demo.taler.net/",
exchangeBaseUrl: "https://exchange.demo.taler.net/",
merchantBaseUrl: "https://backend.demo.taler.net/",
});
@@ -306,7 +306,7 @@ export async function testWithLocal(path: string) {
await w.wallet.client.call(WalletApiOperation.RunIntegrationTest, {
amountToSpend: "TESTKUDOS:1",
amountToWithdraw: "TESTKUDOS:3",
- bankAccessApiBaseUrl: "http://localhost:8082/taler-bank-access/",
+ corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
exchangeBaseUrl: "http://localhost:8081/",
merchantBaseUrl: "http://localhost:8083/",
});
diff --git a/packages/taler-wallet-webextension/manifest-common.json b/packages/taler-wallet-webextension/manifest-common.json
index bb752e1b7..0e333c7a8 100644
--- a/packages/taler-wallet-webextension/manifest-common.json
+++ b/packages/taler-wallet-webextension/manifest-common.json
@@ -2,8 +2,8 @@
"name": "GNU Taler Wallet (git)",
"description": "Privacy preserving and transparent payments",
"author": "GNU Taler Developers",
- "version": "0.9.3.17",
- "version_name": "0.9.3-dev.17",
+ "version": "0.9.3.27",
+ "version_name": "0.9.3-dev.27",
"icons": {
"16": "static/img/taler-logo-16.png",
"19": "static/img/taler-logo-19.png",
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
index 23736729c..fc91ed4cc 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-wallet-webextension",
- "version": "0.9.3-dev.17",
+ "version": "0.9.3-dev.27",
"description": "GNU Taler Wallet browser extension",
"main": "./build/index.js",
"types": "./build/index.d.ts",
@@ -75,4 +75,4 @@
"pogen": {
"domain": "taler-wallet-webex"
}
-}
+} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
index c2cef451b..d3733e6cc 100644
--- a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
+++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
@@ -14,15 +14,15 @@
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 { Amounts, WalletBalance } from "@gnu-taler/taler-util";
+import { 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 (
diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
index e072d2581..72881c746 100644
--- a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
+++ b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
@@ -62,10 +62,10 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
WithdrawalType.TalerBankIntegrationApi
? !tx.withdrawalDetails.confirmed
? i18n.str`Need approval in the Bank`
- : i18n.str`Exchange is waiting the wire transfer`
+ : i18n.str`Waiting for wire transfer to complete`
: tx.withdrawalDetails.type === WithdrawalType.ManualTransfer
- ? i18n.str`Exchange is waiting the wire transfer`
- : "" //pending but no message
+ ? i18n.str`Waiting for wire transfer to complete`
+ : "" //pending but no message
: undefined
}
/>
@@ -88,8 +88,8 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
? 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
+ ? i18n.str`Exchange is waiting the wire transfer`
+ : "" //pending but no message
: undefined
}
/>
@@ -267,14 +267,14 @@ function Layout(props: LayoutProps): VNode {
style={{
backgroundColor:
props.currentState === TransactionMajorState.Pending ||
- props.currentState === TransactionMajorState.Dialog
+ props.currentState === TransactionMajorState.Dialog
? "lightcyan"
: props.currentState === TransactionMajorState.Failed
- ? "#ff000040"
- : props.currentState === TransactionMajorState.Aborted ||
- props.currentState === TransactionMajorState.Aborting
- ? "#00000010"
- : "inherit",
+ ? "#ff000040"
+ : props.currentState === TransactionMajorState.Aborted ||
+ props.currentState === TransactionMajorState.Aborting
+ ? "#00000010"
+ : "inherit",
alignItems: "center",
}}
>
@@ -353,10 +353,10 @@ function TransactionAmount(props: TransactionAmountProps): VNode {
props.currentState !== TransactionMajorState.Done
? "gray"
: sign === "+"
- ? "darkgreen"
- : sign === "-"
- ? "darkred"
- : undefined,
+ ? "darkgreen"
+ : sign === "-"
+ ? "darkred"
+ : undefined,
}}
>
<ExtraLargeText>
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
index f6c176550..f8e8b1eba 100644
--- a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
@@ -99,7 +99,6 @@ export function ShowButtonsNonAcceptedTosView({
</WarningText>
</section>
)} */}
- {terms.status === ExchangeTosStatus.Accepted && (
<section style={{ justifyContent: "space-around", display: "flex" }}>
<Button
variant="contained"
@@ -109,7 +108,6 @@ export function ShowButtonsNonAcceptedTosView({
<i18n.Translate>Review exchange terms of service</i18n.Translate>
</Button>
</section>
- )}
</Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index a8d2d6fcf..23614e290 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -14,25 +14,29 @@
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 { MultiActionButton } from "../components/MultiActionButton.js";
import {
- alertFromError,
ErrorAlert,
+ 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 { 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 { NoBalanceHelp } from "./NoBalanceHelp.js";
@@ -64,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>;
diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx
index 900218991..56d0ef7bd 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.tsx
@@ -17,26 +17,26 @@
import {
AbsoluteTime,
Amounts,
- Balance,
NotificationType,
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 { 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 { Time } from "../components/Time.js";
import {
CenteredBoldText,
CenteredText,
DateSeparator,
NiceSelect,
} from "../components/styled/index.js";
-import { Time } from "../components/Time.js";
-import { HistoryItem } from "../components/HistoryItem.js";
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 { Button } from "../mui/Button.js";
import { NoBalanceHelp } from "../popup/NoBalanceHelp.js";
@@ -109,7 +109,7 @@ export function HistoryView({
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
defaultCurrency?: string;
transactions: Transaction[];
- balances: Balance[];
+ balances: WalletBalance[];
}): VNode {
const { i18n } = useTranslationContext();
const { pushAlertOnError } = useAlertContext();
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index ac85fe8eb..5a5203392 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/web-util",
- "version": "0.9.3-dev.17",
+ "version": "0.9.3-dev.27",
"description": "Generic helper functionality for GNU Taler Web Apps",
"type": "module",
"types": "./lib/index.node.d.ts",
@@ -35,6 +35,8 @@
"@babel/preset-react": "^7.22.3",
"@babel/preset-typescript": "^7.21.5",
"@gnu-taler/taler-util": "workspace:*",
+ "@heroicons/react": "^2.0.17",
+ "date-fns": "2.29.3",
"@linaria/babel-preset": "4.4.5",
"@linaria/core": "4.2.10",
"@linaria/esbuild": "4.2.11",
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..92c379459
--- /dev/null
+++ b/packages/web-util/src/forms/DefaultForm.tsx
@@ -0,0 +1,65 @@
+
+import { ComponentChildren, Fragment, h } from "preact";
+import { FormProvider, FormState } from "./FormProvider.js";
+import { DoubleColumnForm, RenderAllFieldsByUiConfig } from "./forms.js";
+
+
+export interface FlexibleForm<T extends object> {
+ versionId: string;
+ design: DoubleColumnForm;
+ behavior: (form: Partial<T>) => FormState<T>;
+}
+
+export function DefaultForm<T extends object>({
+ initial,
+ onUpdate,
+ form,
+ onSubmit,
+ children,
+}: {
+ children?: ComponentChildren;
+ initial: Partial<T>;
+ onSubmit?: (v: Partial<T>) => void;
+ form: FlexibleForm<T>;
+ onUpdate?: (d: Partial<T>) => void;
+}) {
+ return (
+ <FormProvider
+ initialValue={initial}
+ onUpdate={onUpdate}
+ onSubmit={onSubmit}
+ 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/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx
new file mode 100644
index 000000000..3da2a4f07
--- /dev/null
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -0,0 +1,99 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { ComponentChildren, VNode, createContext, h } from "preact";
+import {
+ MutableRef,
+ StateUpdater,
+ useEffect,
+ useRef,
+ useState,
+} from "preact/hooks";
+
+export interface FormType<T> {
+ value: MutableRef<Partial<T>>;
+ initialValue?: Partial<T>;
+ onUpdate?: StateUpdater<T>;
+ computeFormState?: (v: T) => FormState<T>;
+}
+
+//@ts-ignore
+export const FormContext = createContext<FormType<any>>({});
+
+export type FormState<T> = {
+ [field in keyof T]?: T[field] extends AbsoluteTime
+ ? Partial<InputFieldState>
+ : T[field] extends AmountJson
+ ? Partial<InputFieldState>
+ : T[field] extends Array<infer P>
+ ? Partial<InputArrayFieldState<P>>
+ : T[field] extends (object | undefined)
+ ? FormState<T[field]>
+ : Partial<InputFieldState>;
+};
+
+export interface InputFieldState {
+ /* should show the error */
+ error?: TranslatedString;
+ /* should not allow to edit */
+ readonly: boolean;
+ /* should show as disable */
+ disabled: boolean;
+ /* should not show */
+ hidden: boolean;
+}
+
+export interface InputArrayFieldState<T> extends InputFieldState {
+ elements: FormState<T>[];
+}
+
+export function FormProvider<T>({
+ children,
+ initialValue,
+ onUpdate: notify,
+ onSubmit,
+ computeFormState,
+}: {
+ initialValue?: Partial<T>;
+ onUpdate?: (v: Partial<T>) => void;
+ onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void;
+ computeFormState?: (v: Partial<T>) => FormState<T>;
+ children: ComponentChildren;
+}): VNode {
+ // const value = useRef(initialValue ?? {});
+ // useEffect(() => {
+ // return function onUnload() {
+ // value.current = initialValue ?? {};
+ // };
+ // });
+ // const onUpdate = notify
+ const [state, setState] = useState<Partial<T>>(initialValue ?? {});
+ const value = { current: state };
+ // console.log("RENDER", initialValue, value);
+ const onUpdate = (v: typeof state) => {
+ // console.log("updated");
+ setState(v);
+ if (notify) notify(v);
+ };
+ return (
+ <FormContext.Provider
+ value={{ initialValue, value, onUpdate, computeFormState }}
+ >
+ <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/InputAmount.tsx b/packages/web-util/src/forms/InputAmount.tsx
new file mode 100644
index 000000000..9be9dd4d0
--- /dev/null
+++ b/packages/web-util/src/forms/InputAmount.tsx
@@ -0,0 +1,34 @@
+import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } 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.parseOrThrow(`${currency}:${v}`);
+ },
+ //@ts-ignore
+ toStringUI: (v: AmountJson) => {
+ return v === undefined ? "" : Amounts.stringifyValue(v);
+ },
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx
new file mode 100644
index 000000000..00379bed6
--- /dev/null
+++ b/packages/web-util/src/forms/InputArray.tsx
@@ -0,0 +1,183 @@
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { FormProvider, InputArrayFieldState } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+import { useField } from "./useField.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+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);
+ }}
+ />
+ );
+ })}
+ <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
+ initialValue={selected}
+ 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.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
new file mode 100644
index 000000000..5c909b5d7
--- /dev/null
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
@@ -0,0 +1,82 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+import { Choice } from "./InputChoiceStacked.js";
+
+export function InputChoiceHorizontal<T extends object, K extends keyof T>(
+ props: {
+ choices: Choice<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"
+ class={clazz}
+ onClick={(e) => {
+ onChange(
+ (value === choice.value ? undefined : choice.value) as T[K],
+ );
+ }}
+ >
+ {(!converter
+ ? (choice.value as string)
+ : converter?.toStringUI(choice.value)) ?? ""}
+ </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.tsx b/packages/web-util/src/forms/InputChoiceStacked.tsx
new file mode 100644
index 000000000..c37984368
--- /dev/null
+++ b/packages/web-util/src/forms/InputChoiceStacked.tsx
@@ -0,0 +1,111 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export interface Choice<V> {
+ label: TranslatedString;
+ description?: TranslatedString;
+ value: V;
+}
+
+export function InputChoiceStacked<T extends object, K extends keyof T>(
+ props: {
+ choices: Choice<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}
+ 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/InputDate.tsx b/packages/web-util/src/forms/InputDate.tsx
new file mode 100644
index 000000000..1fd81aad9
--- /dev/null
+++ b/packages/web-util/src/forms/InputDate.tsx
@@ -0,0 +1,37 @@
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import { InputLine, UIFormProps } from "./InputLine.js";
+import { CalendarIcon } from "@heroicons/react/24/outline";
+import { VNode, h } from "preact";
+import { format, parse } from "date-fns";
+
+export function InputDate<T extends object, K extends keyof T>(
+ props: { pattern?: string } & UIFormProps<T, K>,
+): VNode {
+ const pattern = props.pattern ?? "dd/MM/yyyy";
+ return (
+ <InputLine<T, K>
+ type="text"
+ after={{
+ type: "icon",
+ icon: <CalendarIcon class="h-6 w-6" />,
+ }}
+ converter={{
+ //@ts-ignore
+ fromStringUI: (v): AbsoluteTime => {
+ if (!v) return AbsoluteTime.never();
+ const t_ms = parse(v, pattern, Date.now()).getTime();
+ return AbsoluteTime.fromMilliseconds(t_ms);
+ },
+ //@ts-ignore
+ toStringUI: (v: AbsoluteTime) => {
+ return !v || !v.t_ms
+ ? ""
+ : v.t_ms === "never"
+ ? "never"
+ : format(v.t_ms, pattern);
+ },
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/packages/web-util/src/forms/InputFile.tsx b/packages/web-util/src/forms/InputFile.tsx
new file mode 100644
index 000000000..0d89a98a3
--- /dev/null
+++ b/packages/web-util/src/forms/InputFile.tsx
@@ -0,0 +1,101 @@
+import { Fragment, VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired, UIFormProps } 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,
+ maxBites,
+ accept,
+ } = props;
+ const { value, onChange, state } = useField<T, K>(name);
+
+ 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>
+ <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"
+ />
+
+ <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.tsx b/packages/web-util/src/forms/InputInteger.tsx
new file mode 100644
index 000000000..fb04e3852
--- /dev/null
+++ b/packages/web-util/src/forms/InputInteger.tsx
@@ -0,0 +1,23 @@
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } from "./InputLine.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.tsx b/packages/web-util/src/forms/InputLine.tsx
new file mode 100644
index 000000000..9448ef5e4
--- /dev/null
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -0,0 +1,282 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useField } from "./useField.js";
+
+export interface IconAddon {
+ type: "icon";
+ icon: VNode;
+}
+interface ButtonAddon {
+ type: "button";
+ onClick: () => void;
+ children: ComponentChildren;
+}
+interface TextAddon {
+ type: "text";
+ text: TranslatedString;
+}
+type Addon = IconAddon | ButtonAddon | TextAddon;
+
+interface StringConverter<T> {
+ toStringUI: (v?: T) => string;
+ fromStringUI: (v?: string) => T;
+}
+
+export interface UIFormProps<T extends object, K extends keyof T> {
+ name: K;
+ label: TranslatedString;
+ placeholder?: TranslatedString;
+ tooltip?: TranslatedString;
+ help?: TranslatedString;
+ before?: Addon;
+ after?: Addon;
+ required?: boolean;
+ converter?: StringConverter<T[K]>;
+}
+
+export type FormErrors<T> = {
+ [P in keyof T]?: string | FormErrors<T[P]>;
+};
+
+//@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 flex flex-col items-center hidden mb-6 group-hover:flex">
+ <span class="relative z-10 p-2 text-xs leading-none text-white whitespace-no-wrap bg-black shadow-lg">
+ {tooltip}
+ </span>
+ <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,
+ required,
+}: { error?: string; 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"
+ 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"
+ 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);
+
+ 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";
+ }
+ const fromString: (s: string) => any =
+ converter?.fromStringUI ?? defaultFromString;
+ const toString: (s: any) => string = converter?.toStringUI ?? defaultToString;
+
+ if (type === "text-area") {
+ return (
+ <InputWrapper<T, K>
+ {...props}
+ 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} error={showError ? state.error : undefined}>
+ <input
+ name={String(name)}
+ type={type}
+ 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>
+ );
+}
diff --git a/packages/web-util/src/forms/InputSelectMultiple.tsx b/packages/web-util/src/forms/InputSelectMultiple.tsx
new file mode 100644
index 000000000..8116bdc03
--- /dev/null
+++ b/packages/web-util/src/forms/InputSelectMultiple.tsx
@@ -0,0 +1,151 @@
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Choice } from "./InputChoiceStacked.js";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputSelectMultiple<T extends object, K extends keyof T>(
+ props: {
+ choices: Choice<T[K]>[];
+ unique?: boolean;
+ max?: number;
+ } & UIFormProps<T, K>,
+): VNode {
+ const { name, label, choices, placeholder, tooltip, required, unique, max } =
+ 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 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"
+ 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>
+ );
+ })}
+
+ <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);
+ 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.tsx b/packages/web-util/src/forms/InputSelectOne.tsx
new file mode 100644
index 000000000..7bef1058b
--- /dev/null
+++ b/packages/web-util/src/forms/InputSelectOne.tsx
@@ -0,0 +1,134 @@
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Choice } from "./InputChoiceStacked.js";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputSelectOne<T extends object, K extends keyof T>(
+ props: {
+ choices: Choice<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.tsx b/packages/web-util/src/forms/InputText.tsx
new file mode 100644
index 000000000..1b37ee6fb
--- /dev/null
+++ b/packages/web-util/src/forms/InputText.tsx
@@ -0,0 +1,8 @@
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } 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.tsx b/packages/web-util/src/forms/InputTextArea.tsx
new file mode 100644
index 000000000..45229951e
--- /dev/null
+++ b/packages/web-util/src/forms/InputTextArea.tsx
@@ -0,0 +1,8 @@
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } from "./InputLine.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/forms.ts b/packages/web-util/src/forms/forms.ts
new file mode 100644
index 000000000..2c90a69ed
--- /dev/null
+++ b/packages/web-util/src/forms/forms.ts
@@ -0,0 +1,135 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { InputText } from "./InputText.js";
+import { InputDate } from "./InputDate.js";
+import { InputInteger } from "./InputInteger.js";
+import { h as create, Fragment, VNode } from "preact";
+import { InputChoiceStacked } from "./InputChoiceStacked.js";
+import { InputArray } from "./InputArray.js";
+import { InputSelectMultiple } from "./InputSelectMultiple.js";
+import { InputTextArea } from "./InputTextArea.js";
+import { InputFile } from "./InputFile.js";
+import { Caption } from "./Caption.js";
+import { Group } from "./Group.js";
+import { InputSelectOne } from "./InputSelectOne.js";
+import { FormProvider } from "./FormProvider.js";
+import { InputLine } from "./InputLine.js";
+import { InputAmount } from "./InputAmount.js";
+import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
+
+export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>;
+
+export type DoubleColumnFormSection = {
+ title: TranslatedString;
+ description?: TranslatedString;
+ fields: UIFormField[];
+};
+
+/**
+ * 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];
+ date: Parameters<typeof InputDate<T, K>>[0];
+ integer: Parameters<typeof InputInteger<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: "date"; props: FieldType["date"] };
+
+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
+ date: InputDate,
+ //@ts-ignore
+ choiceStacked: InputChoiceStacked,
+ //@ts-ignore
+ choiceHorizontal: InputChoiceHorizontal,
+ integer: InputInteger,
+ //@ts-ignore
+ selectOne: InputSelectOne,
+ //@ts-ignore
+ selectMultiple: InputSelectMultiple,
+ //@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
+ >;
+};
+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.ts b/packages/web-util/src/forms/index.ts
new file mode 100644
index 000000000..08bb9ee77
--- /dev/null
+++ b/packages/web-util/src/forms/index.ts
@@ -0,0 +1,19 @@
+export * from "./Caption.js"
+export * from "./FormProvider.js"
+export * from "./forms.js"
+export * from "./Group.js"
+export * from "./index.js"
+export * from "./InputAmount.js"
+export * from "./InputArray.js"
+export * from "./InputChoiceHorizontal.js"
+export * from "./InputChoiceStacked.js"
+export * from "./InputDate.js"
+export * from "./InputFile.js"
+export * from "./InputInteger.js"
+export * from "./InputLine.js"
+export * from "./InputSelectMultiple.js"
+export * from "./InputSelectOne.js"
+export * from "./InputTextArea.js"
+export * from "./InputText.js"
+export * from "./useField.js"
+export * from "./DefaultForm.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..bf94d2f5d
--- /dev/null
+++ b/packages/web-util/src/forms/useField.ts
@@ -0,0 +1,93 @@
+import { useContext, useState } from "preact/compat";
+import { FormContext, InputFieldState } from "./FormProvider.js";
+
+export interface InputFieldHandler<Type> {
+ value: Type;
+ onChange: (s: Type) => void;
+ state: InputFieldState;
+ isDirty: boolean;
+}
+
+export function useField<T extends object, K extends keyof T>(
+ name: K,
+): InputFieldHandler<T[K]> {
+ const {
+ initialValue,
+ value: formValue,
+ computeFormState,
+ onUpdate: notifyUpdate,
+ } = 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<InputFieldState>>(formState, String(name)) ?? {};
+
+ //compute default state
+ const state = {
+ disabled: fieldState.disabled ?? false,
+ readonly: fieldState.readonly ?? false,
+ hidden: fieldState.hidden ?? false,
+ error: fieldState.error,
+ 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 a3a2053e6..f6c74ff22 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,11 +1,7 @@
export { useLang } from "./useLang.js";
export { useLocalStorage, buildStorageKey } from "./useLocalStorage.js";
export { useMemoryStorage } from "./useMemoryStorage.js";
-export {
- useNotifications,
- notifyError,
- notifyInfo,
-} from "./useNotifications.js";
+export * from "./useNotifications.js";
export {
useAsyncAsHook,
HookError,
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
index 733950592..e9e8a240b 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -4,13 +4,13 @@ import { memoryMap } from "../index.browser.js";
export type NotificationMessage = ErrorNotification | InfoNotification;
-interface ErrorNotification {
+export interface ErrorNotification {
type: "error";
title: TranslatedString;
description?: TranslatedString;
debug?: string;
}
-interface InfoNotification {
+export interface InfoNotification {
type: "info";
title: TranslatedString;
}
@@ -18,33 +18,43 @@ interface InfoNotification {
const storage = memoryMap<Map<string, NotificationMessage>>();
const NOTIFICATION_KEY = "notification";
+export function notify(notif: NotificationMessage): void {
+ const currentState: Map<string, NotificationMessage> =
+ storage.get(NOTIFICATION_KEY) ?? new Map();
+ const newState = currentState.set(hash(notif), notif);
+ storage.set(NOTIFICATION_KEY, newState);
+}
export function notifyError(
title: TranslatedString,
description: TranslatedString | undefined,
debug?: any,
) {
- const currentState: Map<string, NotificationMessage> =
- storage.get(NOTIFICATION_KEY) ?? new Map();
-
- const notif = {
+ notify({
type: "error" as const,
title,
description,
debug,
- };
- const newState = currentState.set(hash(notif), notif);
- storage.set(NOTIFICATION_KEY, newState);
+ });
+}
+export function notifyException(
+ title: TranslatedString,
+ ex: Error,
+) {
+ notify({
+ type: "error" as const,
+ title,
+ description: ex.message as TranslatedString,
+ debug: ex.stack,
+ });
}
export function notifyInfo(title: TranslatedString) {
- const currentState: Map<string, NotificationMessage> =
- storage.get(NOTIFICATION_KEY) ?? new Map();
-
- const notif = { type: "info" as const, title };
- const newState = currentState.set(hash(notif), notif);
- storage.set(NOTIFICATION_KEY, newState);
+ notify({
+ type: "info" as const,
+ title,
+ });
}
-type Notification = {
+export type Notification = {
message: NotificationMessage;
remove: () => void;
};
@@ -54,7 +64,7 @@ export function useNotifications(): Notification[] {
useEffect(() => {
return storage.onUpdate(NOTIFICATION_KEY, () => {
const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
- setter(mem);
+ setter(structuredClone(mem));
});
});
diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts
index 2a537b405..82c399bfd 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -5,4 +5,5 @@ export * from "./utils/http-impl.sw.js";
export * from "./utils/observable.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
index 6ee1be20a..0e8e7cec3 100644
--- a/packages/web-util/src/index.build.ts
+++ b/packages/web-util/src/index.build.ts
@@ -54,7 +54,7 @@ if (GIT_ROOT === "/") {
// eslint-disable-next-line no-undef
process.exit(1);
}
-const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash();
+const GIT_HASH = git_hash();
const buf = fs.readFileSync(path.join(BASE, "package.json"));
let _package = JSON.parse(buf.toString("utf-8"));
diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts
index 8ce21b0e1..ef4d8e847 100644
--- a/packages/web-util/src/utils/request.ts
+++ b/packages/web-util/src/utils/request.ts
@@ -48,7 +48,7 @@ export async function defaultRequestHandler<T>(
)}`;
}
requestHeaders["Content-Type"] =
- options.contentType === "json" ? "application/json" : "text/plain";
+ !options.contentType || options.contentType === "json" ? "application/json" : "text/plain";
if (options.talerAmlOfficerSignature) {
requestHeaders["Taler-AML-Officer-Signature"] =