summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rwxr-xr-xpackages/aml-backoffice-ui/build.mjs4
-rw-r--r--packages/aml-backoffice-ui/copyleft-header.js2
-rwxr-xr-xpackages/aml-backoffice-ui/dev.mjs6
-rw-r--r--packages/aml-backoffice-ui/package.json17
-rw-r--r--packages/aml-backoffice-ui/src/App.tsx156
-rw-r--r--packages/aml-backoffice-ui/src/Dashboard.tsx234
-rw-r--r--packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx273
-rw-r--r--packages/aml-backoffice-ui/src/Routing.tsx151
-rw-r--r--packages/aml-backoffice-ui/src/context/config.ts98
-rw-r--r--packages/aml-backoffice-ui/src/context/ui-forms.ts76
-rw-r--r--packages/aml-backoffice-ui/src/context/ui-settings.ts110
-rw-r--r--packages/aml-backoffice-ui/src/declaration.d.ts15
-rw-r--r--packages/aml-backoffice-ui/src/forms.json532
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_11e.ts38
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_12e.ts147
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_13e.ts177
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_15e.ts59
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_1e.ts360
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_4e.ts135
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_5e.ts86
-rw-r--r--packages/aml-backoffice-ui/src/forms/902_9e.ts49
-rw-r--r--packages/aml-backoffice-ui/src/forms/declaration.ts70
-rw-r--r--packages/aml-backoffice-ui/src/forms/icons.tsx15
-rw-r--r--packages/aml-backoffice-ui/src/forms/index.ts113
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts124
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts227
-rw-r--r--packages/aml-backoffice-ui/src/hooks/officer.ts (renamed from packages/aml-backoffice-ui/src/hooks/useOfficer.ts)97
-rw-r--r--packages/aml-backoffice-ui/src/hooks/preferences.ts (renamed from packages/aml-backoffice-ui/src/hooks/useSettings.ts)75
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useBackend.ts92
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts82
-rw-r--r--packages/aml-backoffice-ui/src/hooks/useCases.ts144
-rw-r--r--packages/aml-backoffice-ui/src/i18n/bank.pot2
-rw-r--r--packages/aml-backoffice-ui/src/i18n/fr.po2
-rw-r--r--packages/aml-backoffice-ui/src/i18n/poheader2
-rw-r--r--packages/aml-backoffice-ui/src/i18n/strings-prelude2
-rw-r--r--packages/aml-backoffice-ui/src/i18n/strings.ts2
-rw-r--r--packages/aml-backoffice-ui/src/index.html3
-rw-r--r--packages/aml-backoffice-ui/src/index.tsx9
-rw-r--r--packages/aml-backoffice-ui/src/pages.ts44
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx104
-rw-r--r--packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx160
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx458
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx284
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.stories.tsx25
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx441
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx219
-rw-r--r--packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx40
-rw-r--r--packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx104
-rw-r--r--packages/aml-backoffice-ui/src/pages/Officer.tsx42
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx179
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx148
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx162
-rw-r--r--packages/aml-backoffice-ui/src/pages/index.stories.ts16
-rw-r--r--packages/aml-backoffice-ui/src/route.ts197
-rw-r--r--packages/aml-backoffice-ui/src/settings.json4
-rw-r--r--packages/aml-backoffice-ui/src/stories.test.ts30
-rw-r--r--packages/aml-backoffice-ui/src/stories.tsx60
-rw-r--r--packages/aml-backoffice-ui/src/utils/QR.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/utils/converter.ts31
-rw-r--r--packages/aml-backoffice-ui/src/utils/types.ts124
-rw-r--r--packages/anastasis-cli/package.json2
-rw-r--r--packages/anastasis-core/package.json2
-rw-r--r--packages/anastasis-core/src/anastasis-data.ts12
-rw-r--r--packages/anastasis-core/src/index.ts41
-rw-r--r--packages/anastasis-core/tsconfig.json2
-rw-r--r--packages/anastasis-webui/package.json2
-rw-r--r--packages/anastasis-webui/src/components/menu/SideBar.tsx5
-rw-r--r--packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts6
-rw-r--r--packages/anastasis-webui/src/index.html2
-rw-r--r--packages/anastasis-webui/src/index.ts2
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts89
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts24
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx4
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx7
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx13
-rw-r--r--packages/auditor-backoffice-ui/package.json4
-rw-r--r--packages/auditor-backoffice-ui/src/hooks/testing.tsx2
-rw-r--r--packages/auditor-backoffice-ui/src/index.html2
-rw-r--r--packages/bank-ui/README.md2
-rw-r--r--packages/bank-ui/package.json4
-rw-r--r--packages/bank-ui/postcss.config.js15
-rw-r--r--packages/bank-ui/src/Routing.tsx24
-rw-r--r--packages/bank-ui/src/app.tsx62
-rw-r--r--packages/bank-ui/src/components/Transactions/index.ts24
-rw-r--r--packages/bank-ui/src/components/Transactions/state.ts22
-rw-r--r--packages/bank-ui/src/components/Transactions/views.tsx8
-rw-r--r--packages/bank-ui/src/context/config.ts318
-rw-r--r--packages/bank-ui/src/context/navigation.ts92
-rw-r--r--packages/bank-ui/src/context/settings.ts8
-rw-r--r--packages/bank-ui/src/hooks/account.ts123
-rw-r--r--packages/bank-ui/src/hooks/form.ts9
-rw-r--r--packages/bank-ui/src/hooks/regional.ts97
-rw-r--r--packages/bank-ui/src/hooks/session.ts5
-rw-r--r--packages/bank-ui/src/i18n/bank.pot2
-rw-r--r--packages/bank-ui/src/i18n/de.po10
-rw-r--r--packages/bank-ui/src/i18n/en.po1784
-rw-r--r--packages/bank-ui/src/i18n/es.po2
-rw-r--r--packages/bank-ui/src/pages/BankFrame.tsx13
-rw-r--r--packages/bank-ui/src/pages/LoginForm.tsx42
-rw-r--r--packages/bank-ui/src/pages/OperationState/index.ts16
-rw-r--r--packages/bank-ui/src/pages/OperationState/state.ts10
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx111
-rw-r--r--packages/bank-ui/src/pages/ProfileNavigation.tsx5
-rw-r--r--packages/bank-ui/src/pages/PublicHistoriesPage.tsx10
-rw-r--r--packages/bank-ui/src/pages/QrCodeSection.tsx4
-rw-r--r--packages/bank-ui/src/pages/RegistrationPage.tsx16
-rw-r--r--packages/bank-ui/src/pages/SolveChallengePage.tsx7
-rw-r--r--packages/bank-ui/src/pages/WalletWithdrawForm.tsx19
-rw-r--r--packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx42
-rw-r--r--packages/bank-ui/src/pages/WithdrawalOperationPage.tsx4
-rw-r--r--packages/bank-ui/src/pages/account/CashoutListForAccount.tsx3
-rw-r--r--packages/bank-ui/src/pages/account/ShowAccountDetails.tsx194
-rw-r--r--packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx4
-rw-r--r--packages/bank-ui/src/pages/admin/AccountForm.tsx75
-rw-r--r--packages/bank-ui/src/pages/admin/AccountList.tsx8
-rw-r--r--packages/bank-ui/src/pages/admin/AdminHome.tsx143
-rw-r--r--packages/bank-ui/src/pages/admin/CreateNewAccount.tsx13
-rw-r--r--packages/bank-ui/src/pages/admin/DownloadStats.tsx12
-rw-r--r--packages/bank-ui/src/pages/admin/RemoveAccount.tsx4
-rw-r--r--packages/bank-ui/src/pages/regional/ConversionConfig.tsx8
-rw-r--r--packages/bank-ui/src/pages/regional/CreateCashout.tsx76
-rw-r--r--packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx2
-rw-r--r--packages/bank-ui/src/route.ts139
-rw-r--r--packages/bank-ui/src/settings.ts18
-rw-r--r--packages/bank-ui/src/stories.test.ts5
-rw-r--r--packages/bank-ui/src/utils.ts6
-rw-r--r--packages/bank-ui/tailwind.config.js16
-rwxr-xr-xpackages/challenger-ui/build.mjs5
-rw-r--r--packages/challenger-ui/copyleft-header.js2
-rwxr-xr-xpackages/challenger-ui/dev.mjs8
-rw-r--r--packages/challenger-ui/package.json43
-rw-r--r--packages/challenger-ui/postcss.config.js15
-rw-r--r--packages/challenger-ui/src/Routing.tsx270
-rw-r--r--packages/challenger-ui/src/app.tsx168
-rw-r--r--packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx132
-rw-r--r--packages/challenger-ui/src/context/settings.ts44
-rw-r--r--packages/challenger-ui/src/hooks/challenge.ts58
-rw-r--r--packages/challenger-ui/src/hooks/session.ts143
-rw-r--r--packages/challenger-ui/src/i18n/challenger-ui.pot199
-rw-r--r--packages/challenger-ui/src/i18n/poheader26
-rw-r--r--packages/challenger-ui/src/i18n/strings.ts90
-rw-r--r--packages/challenger-ui/src/index.html41
-rw-r--r--packages/challenger-ui/src/index.tsx27
-rw-r--r--packages/challenger-ui/src/pages/AnswerChallenge.tsx279
-rw-r--r--packages/challenger-ui/src/pages/AskChallenge.tsx263
-rw-r--r--packages/challenger-ui/src/pages/CallengeCompleted.tsx26
-rw-r--r--packages/challenger-ui/src/pages/Frame.tsx69
-rw-r--r--packages/challenger-ui/src/pages/MissingParams.tsx (renamed from packages/aml-backoffice-ui/src/forms.ts)16
-rw-r--r--packages/challenger-ui/src/pages/NonceNotFound.tsx42
-rw-r--r--packages/challenger-ui/src/pages/Setup.tsx82
-rw-r--r--packages/challenger-ui/src/settings.json3
-rw-r--r--packages/challenger-ui/src/settings.ts83
-rw-r--r--packages/challenger-ui/tailwind.config.js16
-rw-r--r--packages/challenger-ui/tsconfig.json46
-rw-r--r--packages/idb-bridge/package.json2
-rw-r--r--packages/merchant-backend-ui/package.json6
-rw-r--r--packages/merchant-backoffice-ui/package.json5
-rw-r--r--packages/merchant-backoffice-ui/src/Application.tsx307
-rw-r--r--packages/merchant-backoffice-ui/src/Routing.tsx379
-rw-r--r--packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx146
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx3
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx112
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx12
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx34
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx30
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/index.tsx13
-rw-r--r--packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx19
-rw-r--r--packages/merchant-backoffice-ui/src/context/session.ts298
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/backend.ts375
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/bank.ts221
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.test.ts328
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.ts191
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/merchant.ts211
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/order.test.ts321
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/order.ts313
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/otp.ts222
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/preference.ts3
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/product.test.ts144
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/product.ts212
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/templates.ts283
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/testing.tsx20
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/transfer.test.ts126
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/transfer.ts194
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/webhooks.ts221
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/de.po60
-rw-r--r--packages/merchant-backoffice-ui/src/index.html2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx45
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx9
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx19
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx51
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx82
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx189
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx22
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx22
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx70
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx95
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx114
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx54
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx7
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx64
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx7
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx49
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx93
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx142
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx82
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx18
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx32
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx204
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx11
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx18
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx81
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx108
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx9
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx32
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx94
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx54
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx303
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx9
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx18
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx69
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx105
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx57
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx328
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx53
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx11
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx66
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx53
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx125
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx23
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx7
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx20
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx66
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx70
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx11
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx18
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx85
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx55
-rw-r--r--packages/merchant-backoffice-ui/src/paths/login/index.tsx116
-rw-r--r--packages/merchant-backoffice-ui/src/paths/notfound/index.tsx44
-rw-r--r--packages/merchant-backoffice-ui/src/paths/settings/index.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/scss/toggle.scss20
-rw-r--r--packages/merchant-backoffice-ui/src/utils/constants.ts9
-rw-r--r--packages/pogen/package.json2
-rw-r--r--packages/pogen/src/potextract.ts6
-rw-r--r--packages/taler-harness/Makefile4
-rwxr-xr-xpackages/taler-harness/bin/create_merchantAndBankAccount_pdf.sh27
-rw-r--r--packages/taler-harness/bin/pdf-template.html65
-rw-r--r--packages/taler-harness/debian/changelog24
-rw-r--r--packages/taler-harness/package.json2
-rw-r--r--packages/taler-harness/src/bench1.ts13
-rw-r--r--packages/taler-harness/src/bench3.ts12
-rw-r--r--packages/taler-harness/src/harness/harness.ts26
-rw-r--r--packages/taler-harness/src/harness/sync.ts2
-rw-r--r--packages/taler-harness/src/index.ts438
-rw-r--r--packages/taler-harness/src/integrationtests/test-currency-scope.ts3
-rw-r--r--packages/taler-harness/src/integrationtests/test-denom-unoffered.ts13
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-deposit.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-purse.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts1
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-expired.ts11
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-pull-large.ts194
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-push-large.ts177
-rw-r--r--packages/taler-harness/src/integrationtests/test-revocation.ts10
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts150
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts142
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts177
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts149
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-config.ts2
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-dbless.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts154
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts165
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts107
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts191
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts20
-rw-r--r--packages/taler-util/package.json8
-rw-r--r--packages/taler-util/src/MerchantApiClient.ts9
-rw-r--r--packages/taler-util/src/argon2-impl.wasm.ts (renamed from packages/taler-util/src/argon2-impl.node.ts)0
-rw-r--r--packages/taler-util/src/bank-api-client.ts3
-rw-r--r--packages/taler-util/src/codec.ts49
-rw-r--r--packages/taler-util/src/errors.ts12
-rw-r--r--packages/taler-util/src/http-client/authentication.ts11
-rw-r--r--packages/taler-util/src/http-client/bank-conversion.ts10
-rw-r--r--packages/taler-util/src/http-client/bank-core.ts210
-rw-r--r--packages/taler-util/src/http-client/bank-integration.ts22
-rw-r--r--packages/taler-util/src/http-client/bank-revenue.ts78
-rw-r--r--packages/taler-util/src/http-client/bank-wire.ts12
-rw-r--r--packages/taler-util/src/http-client/challenger.ts291
-rw-r--r--packages/taler-util/src/http-client/exchange.ts45
-rw-r--r--packages/taler-util/src/http-client/merchant.ts919
-rw-r--r--packages/taler-util/src/http-client/types.ts727
-rw-r--r--packages/taler-util/src/http-client/utils.ts28
-rw-r--r--packages/taler-util/src/http-common.ts41
-rw-r--r--packages/taler-util/src/http-impl.node.ts8
-rw-r--r--packages/taler-util/src/index.ts1
-rw-r--r--packages/taler-util/src/notifications.ts18
-rw-r--r--packages/taler-util/src/operation.ts24
-rw-r--r--packages/taler-util/src/taler-crypto.ts17
-rw-r--r--packages/taler-util/src/taler-error-codes.ts828
-rw-r--r--packages/taler-util/src/taler-types.ts56
-rw-r--r--packages/taler-util/src/talerconfig.ts151
-rw-r--r--packages/taler-util/src/taleruri.test.ts47
-rw-r--r--packages/taler-util/src/taleruri.ts129
-rw-r--r--packages/taler-util/src/time.ts6
-rw-r--r--packages/taler-util/src/transactions-types.ts40
-rw-r--r--packages/taler-util/src/wallet-types.ts373
-rw-r--r--packages/taler-util/src/whatwg-url.ts9
-rw-r--r--packages/taler-wallet-cli/debian/changelog24
-rw-r--r--packages/taler-wallet-cli/package.json2
-rw-r--r--packages/taler-wallet-cli/src/index.ts140
-rw-r--r--packages/taler-wallet-core/package.json2
-rw-r--r--packages/taler-wallet-core/src/attention.ts70
-rw-r--r--packages/taler-wallet-core/src/backup/index.ts368
-rw-r--r--packages/taler-wallet-core/src/balance.ts88
-rw-r--r--packages/taler-wallet-core/src/coinSelection.test.ts11
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts436
-rw-r--r--packages/taler-wallet-core/src/common.ts101
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts38
-rw-r--r--packages/taler-wallet-core/src/db.ts132
-rw-r--r--packages/taler-wallet-core/src/dbless.ts6
-rw-r--r--packages/taler-wallet-core/src/denomSelection.ts41
-rw-r--r--packages/taler-wallet-core/src/deposits.ts566
-rw-r--r--packages/taler-wallet-core/src/dev-experiments.ts88
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts596
-rw-r--r--packages/taler-wallet-core/src/instructedAmountConversion.ts9
-rw-r--r--packages/taler-wallet-core/src/observable-wrappers.ts51
-rw-r--r--packages/taler-wallet-core/src/pay-merchant.ts785
-rw-r--r--packages/taler-wallet-core/src/pay-peer-common.ts128
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-credit.ts138
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-debit.ts386
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-credit.ts42
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-debit.ts485
-rw-r--r--packages/taler-wallet-core/src/query.ts165
-rw-r--r--packages/taler-wallet-core/src/recoup.ts114
-rw-r--r--packages/taler-wallet-core/src/refresh.ts1241
-rw-r--r--packages/taler-wallet-core/src/reward.ts165
-rw-r--r--packages/taler-wallet-core/src/shepherd.ts174
-rw-r--r--packages/taler-wallet-core/src/testing.ts308
-rw-r--r--packages/taler-wallet-core/src/transactions.ts281
-rw-r--r--packages/taler-wallet-core/src/versions.ts6
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts102
-rw-r--r--packages/taler-wallet-core/src/wallet.ts765
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts661
-rw-r--r--packages/taler-wallet-embedded/package.json2
-rw-r--r--packages/taler-wallet-embedded/src/wallet-qjs.ts31
-rw-r--r--packages/taler-wallet-webextension/manifest-common.json4
-rw-r--r--packages/taler-wallet-webextension/package.json2
-rw-r--r--packages/taler-wallet-webextension/src/NavigationBar.tsx38
-rw-r--r--packages/taler-wallet-webextension/src/components/BalanceTable.tsx61
-rw-r--r--packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx200
-rw-r--r--packages/taler-wallet-webextension/src/components/HistoryItem.tsx68
-rw-r--r--packages/taler-wallet-webextension/src/components/Modal.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/components/PendingTransactions.tsx81
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx15
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx1539
-rw-r--r--packages/taler-wallet-webextension/src/context/alert.ts71
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts73
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts83
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx (renamed from packages/aml-backoffice-ui/src/settings.ts)24
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts65
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx74
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx14
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx1
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx44
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts12
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts44
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts3
-rw-r--r--packages/taler-wallet-webextension/src/i18n/de.po16
-rw-r--r--packages/taler-wallet-webextension/src/mui/Button.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/platform/api.ts51
-rw-r--r--packages/taler-wallet-webextension/src/platform/background.ts3
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts81
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts2
-rw-r--r--packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx11
-rw-r--r--packages/taler-wallet-webextension/src/svg/search_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/test-utils.ts2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts9
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts11
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts7
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx115
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Application.tsx82
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts7
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx14
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.stories.tsx257
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.tsx257
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx38
-rw-r--r--packages/taler-wallet-webextension/src/wallet/QrReader.tsx136
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx855
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts12
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts253
-rw-r--r--packages/web-util/package.json2
-rw-r--r--packages/web-util/src/components/Button.tsx176
-rw-r--r--packages/web-util/src/components/ErrorLoadingMerchant.tsx147
-rw-r--r--packages/web-util/src/context/activity.ts21
-rw-r--r--packages/web-util/src/context/api.ts3
-rw-r--r--packages/web-util/src/context/bank-api.ts77
-rw-r--r--packages/web-util/src/context/challenger-api.ts213
-rw-r--r--packages/web-util/src/context/exchange-api.ts217
-rw-r--r--packages/web-util/src/context/index.ts2
-rw-r--r--packages/web-util/src/context/merchant-api.ts87
-rw-r--r--packages/web-util/src/context/navigation.ts46
-rw-r--r--packages/web-util/src/forms/Calendar.tsx253
-rw-r--r--packages/web-util/src/forms/Caption.tsx17
-rw-r--r--packages/web-util/src/forms/DefaultForm.tsx17
-rw-r--r--packages/web-util/src/forms/FormProvider.tsx48
-rw-r--r--packages/web-util/src/forms/Group.tsx47
-rw-r--r--packages/web-util/src/forms/InputAbsoluteTime.stories.tsx8
-rw-r--r--packages/web-util/src/forms/InputAbsoluteTime.tsx61
-rw-r--r--packages/web-util/src/forms/InputAmount.stories.tsx6
-rw-r--r--packages/web-util/src/forms/InputAmount.tsx33
-rw-r--r--packages/web-util/src/forms/InputArray.stories.tsx10
-rw-r--r--packages/web-util/src/forms/InputArray.tsx32
-rw-r--r--packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx6
-rw-r--r--packages/web-util/src/forms/InputChoiceHorizontal.tsx26
-rw-r--r--packages/web-util/src/forms/InputChoiceStacked.stories.tsx6
-rw-r--r--packages/web-util/src/forms/InputChoiceStacked.tsx14
-rw-r--r--packages/web-util/src/forms/InputFile.stories.tsx6
-rw-r--r--packages/web-util/src/forms/InputFile.tsx67
-rw-r--r--packages/web-util/src/forms/InputInteger.stories.tsx6
-rw-r--r--packages/web-util/src/forms/InputLine.stories.tsx6
-rw-r--r--packages/web-util/src/forms/InputLine.tsx120
-rw-r--r--packages/web-util/src/forms/InputSelectMultiple.stories.tsx8
-rw-r--r--packages/web-util/src/forms/InputSelectMultiple.tsx174
-rw-r--r--packages/web-util/src/forms/InputSelectOne.stories.tsx6
-rw-r--r--packages/web-util/src/forms/InputSelectOne.tsx27
-rw-r--r--packages/web-util/src/forms/InputText.stories.tsx6
-rw-r--r--packages/web-util/src/forms/InputTextArea.stories.tsx6
-rw-r--r--packages/web-util/src/forms/InputToggle.stories.tsx6
-rw-r--r--packages/web-util/src/forms/InputToggle.tsx50
-rw-r--r--packages/web-util/src/forms/converter.ts119
-rw-r--r--packages/web-util/src/forms/forms.ts304
-rw-r--r--packages/web-util/src/forms/index.ts2
-rw-r--r--packages/web-util/src/forms/ui-form.ts363
-rw-r--r--packages/web-util/src/forms/useField.ts36
-rw-r--r--packages/web-util/src/hooks/index.ts2
-rw-r--r--packages/web-util/src/hooks/useLocalStorage.ts7
-rw-r--r--packages/web-util/src/hooks/useNotifications.ts23
-rw-r--r--packages/web-util/src/tests/mock.ts7
-rw-r--r--packages/web-util/src/tests/swr.ts1
-rw-r--r--packages/web-util/src/utils/http-impl.sw.ts7
-rw-r--r--packages/web-util/src/utils/request.ts43
-rw-r--r--packages/web-util/src/utils/route.ts9
456 files changed, 26859 insertions, 17296 deletions
diff --git a/packages/aml-backoffice-ui/build.mjs b/packages/aml-backoffice-ui/build.mjs
index bd7a088cf..04a6f646b 100755
--- a/packages/aml-backoffice-ui/build.mjs
+++ b/packages/aml-backoffice-ui/build.mjs
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -20,7 +20,7 @@ import { build } from "@gnu-taler/web-util/build";
await build({
type: "production",
source: {
- js: ["src/index.tsx", "src/forms.ts"],
+ js: ["src/index.tsx"],
assets: [{ base: "src", files: ["src/index.html"] }],
},
destination: "./dist/prod",
diff --git a/packages/aml-backoffice-ui/copyleft-header.js b/packages/aml-backoffice-ui/copyleft-header.js
index 2635717c5..7fa276bea 100644
--- a/packages/aml-backoffice-ui/copyleft-header.js
+++ b/packages/aml-backoffice-ui/copyleft-header.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/aml-backoffice-ui/dev.mjs b/packages/aml-backoffice-ui/dev.mjs
index bc6fcd6c1..e91b48f9d 100755
--- a/packages/aml-backoffice-ui/dev.mjs
+++ b/packages/aml-backoffice-ui/dev.mjs
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -18,13 +18,13 @@
import { serve } from "@gnu-taler/web-util/node";
import { initializeDev } from "@gnu-taler/web-util/build";
-const devEntryPoints = ["src/stories.tsx", "src/index.tsx", "src/forms.ts"];
+const devEntryPoints = ["src/stories.tsx", "src/index.tsx"];
const build = initializeDev({
type: "development",
source: {
js: devEntryPoints,
- assets: [{ base: "src", files: ["src/index.html"] }],
+ assets: [{ base: "src", files: ["src/index.html","src/forms.json","src/settings.json"] }],
},
destination: "./dist/dev",
css: "postcss",
diff --git a/packages/aml-backoffice-ui/package.json b/packages/aml-backoffice-ui/package.json
index b4df017ea..749565946 100644
--- a/packages/aml-backoffice-ui/package.json
+++ b/packages/aml-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/aml-backoffice-ui",
- "version": "0.1.0",
+ "version": "0.10.7",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "Back-office SPA for GNU Taler Exchange.",
@@ -29,24 +29,23 @@
"history": "4.10.1",
"jed": "1.1.1",
"preact": "10.11.3",
- "swr": "2.0.3"
+ "swr": "2.2.2"
},
"devDependencies": {
- "eslint": "^8.56.0",
- "@typescript-eslint/eslint-plugin": "^6.19.0",
- "@typescript-eslint/parser": "^6.19.0",
- "eslint-config-prettier": "^9.1.0",
- "eslint-plugin-react": "^7.33.2",
-
- "@gnu-taler/pogen": "^0.0.5",
+ "@gnu-taler/pogen": "workspace:*",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"@types/chai": "^4.3.0",
"@types/history": "^4.7.8",
"@types/mocha": "^10.0.1",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
"autoprefixer": "^10.4.14",
"chai": "^4.3.6",
"esbuild": "^0.19.9",
+ "eslint": "^8.56.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
"mocha": "^9.2.0",
"po2json": "^0.4.5",
"postcss": "^8.4.23",
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
index d461934c0..e9be84441 100644
--- a/packages/aml-backoffice-ui/src/App.tsx
+++ b/packages/aml-backoffice-ui/src/App.tsx
@@ -1,32 +1,138 @@
-import { TranslationProvider } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
-import { ExchangeAmlFrame } from "./Dashboard.js";
-import "./scss/main.css";
-import { ExchangeApiProvider } from "./context/config.js";
-import { getInitialBackendBaseURL } from "./hooks/useBackend.js";
-import { HashPathProvider, Router } from "./route.js";
-import { Pages } from "./pages.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
-const pageList = Object.values(Pages);
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
+import {
+ BrowserHashNavigationProvider,
+ ExchangeApiProvider,
+ Loading,
+ TranslationProvider,
+ UiForms,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { SWRConfig } from "swr";
+import { ExchangeAmlFrame } from "./ExchangeAmlFrame.js";
+import { Routing } from "./Routing.js";
+import { UiSettingsProvider } from "./context/ui-settings.js";
+import { strings } from "./i18n/strings.js";
+import "./scss/main.css";
+import { UiSettings, fetchUiSettings } from "./context/ui-settings.js";
+import { UiFormsProvider, fetchUiForms } from "./context/ui-forms.js";
+const WITH_LOCAL_STORAGE_CACHE = false;
export function App(): VNode {
- const baseUrl = getInitialBackendBaseURL();
+ const [settings, setSettings] = useState<UiSettings>();
+ const [forms, setForms] = useState<UiForms>();
+ useEffect(() => {
+ fetchUiSettings(setSettings);
+ fetchUiForms(setForms);
+ }, []);
+ if (!settings || !forms) return <Loading />;
+
+ const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
return (
- <TranslationProvider source={{}}>
- <ExchangeApiProvider baseUrl={baseUrl} frameOnError={ExchangeAmlFrame}>
- <HashPathProvider>
- <ExchangeAmlFrame>
- <Router
- pageList={pageList}
- onNotFound={() => {
- window.location.href = Pages.cases.url
- return <div>not found</div>;
- }}
- />
- </ExchangeAmlFrame>
- </HashPathProvider>
- </ExchangeApiProvider>
- </TranslationProvider>
+ <UiSettingsProvider value={settings}>
+ <TranslationProvider
+ source={strings}
+ completeness={{
+ es: strings["es"].completeness,
+ de: strings["de"].completeness,
+ }}
+ >
+ <ExchangeApiProvider
+ baseUrl={new URL("/", baseUrl)}
+ frameOnError={ExchangeAmlFrame}
+ >
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <BrowserHashNavigationProvider>
+ <UiFormsProvider value={forms}>
+ <Routing />
+ </UiFormsProvider>
+ </BrowserHashNavigationProvider>
+ </SWRConfig>
+ </ExchangeApiProvider>
+ </TranslationProvider>
+ </UiSettingsProvider>
);
}
+
+function localStorageProvider(): Map<unknown, unknown> {
+ const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+ window.addEventListener("beforeunload", () => {
+ const appCache = JSON.stringify(Array.from(map.entries()));
+ localStorage.setItem("app-cache", appCache);
+ });
+ return map;
+}
+
+function getInitialBackendBaseURL(
+ backendFromSettings: string | undefined,
+): string {
+ const overrideUrl =
+ typeof localStorage !== "undefined"
+ ? localStorage.getItem("exchange-base-url")
+ : undefined;
+ let result: string;
+
+ if (!overrideUrl) {
+ // normal path
+ if (!backendFromSettings) {
+ console.error(
+ "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
+ );
+ result = window.origin;
+ } else {
+ result = backendFromSettings;
+ }
+ } else {
+ // testing/development path
+ result = overrideUrl;
+ }
+ try {
+ return canonicalizeBaseUrl(result);
+ } catch (e) {
+ // fall back
+ return canonicalizeBaseUrl(window.origin);
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/Dashboard.tsx b/packages/aml-backoffice-ui/src/Dashboard.tsx
deleted file mode 100644
index 3951b48c7..000000000
--- a/packages/aml-backoffice-ui/src/Dashboard.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { Footer, ToastBanner, Header, notifyError, notifyException, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { useEffect, useErrorBoundary } from "preact/hooks";
-import { useOfficer } from "./hooks/useOfficer.js";
-import { getAllBooleanSettings, getLabelForSetting, useSettings } from "./hooks/useSettings.js";
-import { Pages } from "./pages.js";
-import { PageEntry, useChangeLocation } from "./route.js";
-import { uiSettings } from "./settings.js";
-
-function classNames(...classes: string[]) {
- return classes.filter(Boolean).join(" ");
-}
-
-/**
- * mapping route to view
- * not found (error page)
- * nested, index element, relative routes
- * link interception
- * form POST interception, call action
- * fromData => Object.fromEntries
- * segments in the URL
- * navigationState: idle, submitting, loading
- * form GET interception: does a navigateTo
- * form GET Sync:
- * 1.- back after submit: useEffect to sync URL to form
- * 2.- refresh after submit: input default value
- * useSubmit for form submission onChange, history replace
- *
- * post form without redirect
- *
- *
- * @param param0
- * @returns
- */
-
-const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
-const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
-
-const versionText = VERSION
- ? GIT_HASH
- ? `v${VERSION} (${GIT_HASH.substring(0, 8)})`
- : VERSION
- : "";
-
-/**
- * TO BE FIXED:
- *
- * 1.- when the form change to other form and both form share the same structure
- * the same input component may be rendered in the same place,
- * since input are uncontrolled the are not re-rendered and since they are
- * uncontrolled it will keep the value of the previous form.
- * One solutions could be to remove the form when unloading and when the new
- * form load it will start without previous vdom, preventing the cache
- * to create this behavior.
- * Other solutions could be using IDs in the fields that are constructed
- * with the ID of the form, so two fields of different form will need to re-render
- * cleaning up the state of the previous form.
- *
- * 2.- currently the design prop and the behavior prop of the flexible form
- * are two side of the same coin. From the design point of view, it is important
- * to design the form in a list-of-field manner and there may be additional
- * content that is not directly mapped to the form structure (object)
- * So maybe we want to change the current shape so the computation of the state
- * of the form is in a field level, but this computation required the field value and
- * the whole form values and state (since one field may be disabled/hidden) because
- * of the value of other field.
- *
- * 3.- given the previous requirement, maybe the name of the field of the form could be
- * a function (P: F -> V) where F is the form (or parent object) and V is the type of the
- * property. That will help with the typing of the forms props
- *
- * 4.- tooltip are not placed correctly: the arrow should point the question mark
- * and the text area should be bigger
- *
- */
-
-/**
- * check this fields
- *
- * Signature of Contracting partner, 902_9e
- * Currency and amount of deposited assets, 902_5e
- * Signature on declaration of trust, 902.13e
- * also fundations
- * also life insurance
- *
- * no all state are handled by all the inputs
- * all the input implementation should respect
- * ui props and state
- */
-
-export function ExchangeAmlFrame({
- children,
-}: {
- children?: ComponentChildren;
-}): VNode {
- const { i18n } = useTranslationContext();
-
- const [error, resetError] = useErrorBoundary();
-
- useEffect(() => {
- if (error) {
- if (error instanceof Error) {
- notifyException(i18n.str`Internal error, please report.`, error)
- } else {
- notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString)
- }
- console.log(error)
- // resetError()
- }
- }, [error])
-
- const officer = useOfficer();
- const [settings, updateSettings] = useSettings();
-
- return (<div class="min-h-full flex flex-col m-0 bg-slate-200" style="min-height: 100vh;">
- <div class="bg-indigo-600 pb-32">
- <Header
- title="Exchange"
- iconLinkURL={uiSettings.backendBaseURL ?? "#"}
- onLogout={officer.state !== "ready" ? undefined : () => {
- officer.lock()
- }}
- sites={[]}
- supportedLangs={["en", "es", "de"]}
- >
- <li>
- <div class="text-xs font-semibold leading-6 text-gray-400">
- <i18n.Translate>Preferences</i18n.Translate>
- </div>
- <ul role="list" class="space-y-1">
- {getAllBooleanSettings().map(set => {
- const isOn: boolean = !!settings[set]
- return <li class="mt-2 pl-2">
- <div class="flex items-center justify-between">
- <span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
- {getLabelForSetting(set, i18n)}
- </span>
- </span>
- <button type="button" data-enabled={isOn} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
- onClick={() => { updateSettings(set, !isOn); }}>
- <span aria-hidden="true" data-enabled={isOn} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
- </button>
- </div>
- </li>
- })}
- </ul>
- </li>
- </Header>
- </div>
-
- <div class="fixed z-20 w-full">
- <div class="mx-auto w-4/5">
- <ToastBanner />
- </div>
- </div>
-
- <div class="-mt-32 flex grow ">
- {officer.state !== "ready" ? undefined :
- <Navigation />
- }
- <div class="flex mx-auto my-4">
- <main class="rounded-lg bg-white px-5 py-6 shadow">
- {children}
- </main>
- </div>
-
- </div>
-
- <Footer
- testingUrlKey="exchange-base-url"
- GIT_HASH={GIT_HASH}
- VERSION={VERSION}
- />
- </div>
- );
-}
-
-function Navigation(): VNode {
- const { i18n } = useTranslationContext()
- const pageList: Array<PageEntry> = [
- Pages.officer,
- Pages.cases
- ]
- const location = useChangeLocation();
- return (
- <div class="hidden sm:block min-w-min bg-indigo-600 divide-y rounded-r-lg divide-cyan-800 overflow-y-auto overflow-x-clip">
-
- <nav class="flex flex-1 flex-col mx-4 mt-4 mb-2">
- <ul role="list" class="flex flex-1 flex-col gap-y-7">
- <li>
- <ul role="list" class="-mx-2 space-y-1">
- {pageList.map(p => {
-
- return <li>
- <a href={p.url} data-selected={location == p.url}
- class="data-[selected=true]:bg-indigo-700 pr-4 data-[selected=true]:text-white text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
- {p.Icon && <p.Icon />}
- <span class="hidden md:inline">
- {p.name}
- </span>
- </a>
- </li>
-
- })}
- {/* <li>
- <a href="#" class="text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
-
- <i18n.Translate>Officer</i18n.Translate>
- </a>
- </li> */}
- </ul>
- </li>
-
- {/* <li class="mt-auto ">
- <a href="#" class="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-indigo-200 hover:bg-indigo-700 hover:text-white">
- <svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
- </svg>
- Settings
- </a>
- </li> */}
-
- </ul>
- </nav>
- </div>
- )
-
-}
-
-
diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
new file mode 100644
index 000000000..772fd1b70
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
@@ -0,0 +1,273 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { TranslatedString } from "@gnu-taler/taler-util";
+import {
+ Footer,
+ Header,
+ ToastBanner,
+ notifyError,
+ notifyException,
+ useNavigationContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, VNode, h } from "preact";
+import { useEffect, useErrorBoundary } from "preact/hooks";
+import { privatePages } from "./Routing.js";
+import { useUiSettingsContext } from "./context/ui-settings.js";
+import { OfficerState } from "./hooks/officer.js";
+import {
+ getAllBooleanPreferences,
+ getLabelForPreferences,
+ usePreferences,
+} from "./hooks/preferences.js";
+import { HomeIcon } from "./pages/Cases.js";
+
+/**
+ * mapping route to view
+ * not found (error page)
+ * nested, index element, relative routes
+ * link interception
+ * form POST interception, call action
+ * fromData => Object.fromEntries
+ * segments in the URL
+ * navigationState: idle, submitting, loading
+ * form GET interception: does a navigateTo
+ * form GET Sync:
+ * 1.- back after submit: useEffect to sync URL to form
+ * 2.- refresh after submit: input default value
+ * useSubmit for form submission onChange, history replace
+ *
+ * post form without redirect
+ *
+ *
+ * @param param0
+ * @returns
+ */
+
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
+
+/**
+ * TO BE FIXED:
+ *
+ * 1.- when the form change to other form and both form share the same structure
+ * the same input component may be rendered in the same place,
+ * since input are uncontrolled the are not re-rendered and since they are
+ * uncontrolled it will keep the value of the previous form.
+ * One solutions could be to remove the form when unloading and when the new
+ * form load it will start without previous vdom, preventing the cache
+ * to create this behavior.
+ * Other solutions could be using IDs in the fields that are constructed
+ * with the ID of the form, so two fields of different form will need to re-render
+ * cleaning up the state of the previous form.
+ *
+ * 2.- currently the design prop and the behavior prop of the flexible form
+ * are two side of the same coin. From the design point of view, it is important
+ * to design the form in a list-of-field manner and there may be additional
+ * content that is not directly mapped to the form structure (object)
+ * So maybe we want to change the current shape so the computation of the state
+ * of the form is in a field level, but this computation required the field value and
+ * the whole form values and state (since one field may be disabled/hidden) because
+ * of the value of other field.
+ *
+ * 3.- given the previous requirement, maybe the name of the field of the form could be
+ * a function (P: F -> V) where F is the form (or parent object) and V is the type of the
+ * property. That will help with the typing of the forms props
+ *
+ * 4.- tooltip are not placed correctly: the arrow should point the question mark
+ * and the text area should be bigger
+ *
+ */
+
+/**
+ * check this fields
+ *
+ * Signature of Contracting partner, 902_9e
+ * Currency and amount of deposited assets, 902_5e
+ * Signature on declaration of trust, 902.13e
+ * also fundations
+ * also life insurance
+ *
+ * no all state are handled by all the inputs
+ * all the input implementation should respect
+ * ui props and state
+ */
+
+export function ExchangeAmlFrame({
+ children,
+ officer,
+}: {
+ officer?: OfficerState,
+ children?: ComponentChildren;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [error] = useErrorBoundary();
+
+ useEffect(() => {
+ if (error) {
+ if (error instanceof Error) {
+ notifyException(i18n.str`Internal error, please report.`, error);
+ } else {
+ notifyError(
+ i18n.str`Internal error, please report.`,
+ String(error) as TranslatedString,
+ );
+ }
+ console.log(error);
+ // resetError()
+ }
+ }, [error]);
+
+ const [preferences, updatePreferences] = usePreferences();
+ const settings = useUiSettingsContext()
+
+ return (
+ <div
+ class="min-h-full flex flex-col m-0 bg-slate-200"
+ style="min-height: 100vh;"
+ >
+ <div class="bg-indigo-600 pb-32">
+ <Header
+ title="Exchange"
+ iconLinkURL={settings.backendBaseURL ?? "#"}
+ onLogout={
+ officer?.state !== "ready"
+ ? undefined
+ : () => {
+ officer.lock();
+ }
+ }
+ sites={[]}
+ supportedLangs={["en", "es", "de"]}
+ >
+ <li>
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Preferences</i18n.Translate>
+ </div>
+ <ul role="list" class="space-y-1">
+ {getAllBooleanPreferences().map((set) => {
+ const isOn: boolean = !!preferences[set];
+ return (
+ <li key={set} class="mt-2 pl-2">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ {getLabelForPreferences(set, i18n)}
+ </span>
+ </span>
+ <button
+ type="button"
+ data-enabled={isOn}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ updatePreferences(set, !isOn);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={isOn}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
+ </li>
+ );
+ })}
+ </ul>
+ </li>
+ </Header>
+ </div>
+
+ <div class="fixed z-20 w-full">
+ <div class="mx-auto w-4/5">
+ <ToastBanner />
+ </div>
+ </div>
+
+ <div class="-mt-32 flex grow ">
+ {officer?.state !== "ready" ? undefined : <Navigation />}
+ <div class="flex mx-auto my-4">
+ <main class="rounded-lg bg-white px-5 py-6 shadow">{children}</main>
+ </div>
+ </div>
+
+ <Footer
+ testingUrlKey="exchange-base-url"
+ GIT_HASH={GIT_HASH}
+ VERSION={VERSION}
+ />
+ </div>
+ );
+}
+
+function Navigation(): VNode {
+ const { i18n } = useTranslationContext();
+ const pageList = [
+ { route: privatePages.account, Icon: HomeIcon, label: i18n.str`Account` },
+ { route: privatePages.cases, Icon: HomeIcon, label: i18n.str`Cases` },
+ ];
+ const { path } = useNavigationContext();
+ return (
+ <div class="hidden sm:block min-w-min bg-indigo-600 divide-y rounded-r-lg divide-cyan-800 overflow-y-auto overflow-x-clip">
+ <nav class="flex flex-1 flex-col mx-4 mt-4 mb-2">
+ <ul role="list" class="flex flex-1 flex-col gap-y-7">
+ <li>
+ <ul role="list" class="-mx-2 space-y-1">
+ {pageList.map((p, idx) => {
+ return (
+ <li key={idx}>
+ <a
+ href={p.route.url({})}
+ data-selected={path == p.route.url({})}
+ class="data-[selected=true]:bg-indigo-700 pr-4 data-[selected=true]:text-white text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
+ >
+ {p.Icon && <p.Icon />}
+ <span class="hidden md:inline">{p.label}</span>
+ </a>
+ </li>
+ );
+ })}
+ {/* <li>
+ <a href="#" class="text-indigo-200 hover:text-white hover:bg-indigo-700 group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
+
+ <i18n.Translate>Officer</i18n.Translate>
+ </a>
+ </li> */}
+ </ul>
+ </li>
+
+ {/* <li class="mt-auto ">
+ <a href="#" class="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-indigo-200 hover:bg-indigo-700 hover:text-white">
+ <svg class="h-6 w-6 shrink-0 text-indigo-200 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
+ </svg>
+ Settings
+ </a>
+ </li> */}
+ </ul>
+ </nav>
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx
new file mode 100644
index 000000000..f38fc29c2
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/Routing.tsx
@@ -0,0 +1,151 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ urlPattern,
+ useCurrentLocation,
+ useNavigationContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+import { assertUnreachable } from "@gnu-taler/taler-util";
+import { useEffect } from "preact/hooks";
+import { ExchangeAmlFrame } from "./ExchangeAmlFrame.js";
+import { useOfficer } from "./hooks/officer.js";
+import { Cases } from "./pages/Cases.js";
+import { Officer } from "./pages/Officer.js";
+import { CaseDetails } from "./pages/CaseDetails.js";
+import { CaseUpdate, SelectForm } from "./pages/CaseUpdate.js";
+import { HandleAccountNotReady } from "./pages/HandleAccountNotReady.js";
+
+export function Routing(): VNode {
+ const session = useOfficer();
+
+ if (session.state === "ready") {
+ return (
+ <ExchangeAmlFrame officer={session}>
+ <PrivateRouting />
+ </ExchangeAmlFrame>
+ );
+ }
+ return (
+ <ExchangeAmlFrame>
+ <PublicRounting />
+ </ExchangeAmlFrame>
+ );
+}
+
+const publicPages = {
+ config: urlPattern(/\/config/, () => "#/config"),
+ login: urlPattern(/\/login/, () => "#/login"),
+};
+
+function PublicRounting(): VNode {
+ const { i18n } = useTranslationContext();
+ const location = useCurrentLocation(publicPages);
+ // const { navigateTo } = useNavigationContext();
+ // const { config, lib } = useExchangeApiContext();
+ // const [notification, notify, handleError] = useLocalNotification();
+ const session = useOfficer();
+
+ if (location === undefined) {
+ if (session.state !== "ready") {
+ return <HandleAccountNotReady officer={session}/>;
+ } else {
+ return <div />
+ }
+ }
+
+ switch (location.name) {
+ case "config": {
+ return (
+ <Fragment>
+ <div class="sm:mx-auto sm:w-full sm:max-w-sm">
+ <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to exchange config!`}</h2>
+ </div>
+ </Fragment>
+ );
+ }
+ case "login": {
+ return (
+ <Fragment>
+ <div class="sm:mx-auto sm:w-full sm:max-w-sm">
+ <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to exchange config!`}</h2>
+ </div>
+ </Fragment>
+ );
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
+
+export const privatePages = {
+ account: urlPattern(/\/account/, () => "#/account"),
+ cases: urlPattern(/\/cases/, () => "#/cases"),
+ caseUpdate: urlPattern<{ cid: string; type: string }>(
+ /\/case\/(?<cid>[a-zA-Z0-9]+)\/new\/(?<type>[a-zA-Z0-9_.]+)/,
+ ({ cid, type }) => `#/case/${cid}/new/${type}`,
+ ),
+ caseNew: urlPattern<{ cid: string }>(
+ /\/case\/(?<cid>[a-zA-Z0-9]+)\/new/,
+ ({ cid }) => `#/case/${cid}/new`,
+ ),
+ caseDetails: urlPattern<{ cid: string }>(
+ /\/case\/(?<cid>[a-zA-Z0-9]+)/,
+ ({ cid }) => `#/case/${cid}`,
+ ),
+};
+
+function PrivateRouting(): VNode {
+ const { navigateTo } = useNavigationContext();
+ const location = useCurrentLocation(privatePages);
+ useEffect(() => {
+ if (location === undefined) {
+ navigateTo(privatePages.account.url({}));
+ }
+ }, [location]);
+
+ if (location === undefined) {
+ return <Fragment />;
+ }
+
+ switch (location.name) {
+ case "account": {
+ return <Officer />;
+ }
+ case "caseDetails": {
+ return <CaseDetails account={location.values.cid} />;
+ }
+ case "caseUpdate": {
+ return (
+ <CaseUpdate
+ account={location.values.cid}
+ type={location.values.type}
+ />
+ );
+ }
+ case "caseNew": {
+ return <SelectForm account={location.values.cid} />;
+ }
+ case "cases": {
+ return <Cases />;
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/context/config.ts b/packages/aml-backoffice-ui/src/context/config.ts
deleted file mode 100644
index 0ea491ca4..000000000
--- a/packages/aml-backoffice-ui/src/context/config.ts
+++ /dev/null
@@ -1,98 +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 { TalerExchangeApi, TalerExchangeHttpClient, TalerError } from "@gnu-taler/taler-util";
-import { BrowserFetchHttpLib, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, createContext, FunctionComponent, h, VNode } from "preact";
-import { useContext, useEffect, useState } from "preact/hooks";
-import { ErrorLoading } from "@gnu-taler/web-util/browser";
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-export type Type = {
- url: URL,
- config: TalerExchangeApi.ExchangeVersionResponse,
- api: TalerExchangeHttpClient,
-};
-
-const Context = createContext<Type>(undefined as any);
-
-export const useExchangeApiContext = (): Type => useContext(Context);
-export const useMaybeExchangeApiContext = (): Type | undefined => useContext(Context);
-
-export function ExchangeApiContextTesting({ config, children }: { config: TalerExchangeApi.ExchangeVersionResponse, children?: ComponentChildren; }): VNode {
- return h(Context.Provider, {
- value: { url: new URL("http://testing"), config, api: null as any },
- children
- }
- )
-}
-
-export type ConfigResult = undefined
- | { type: "ok", config: TalerExchangeApi.ExchangeVersionResponse }
- | { type: "incompatible", result: TalerExchangeApi.ExchangeVersionResponse, supported: string }
- | { type: "error", error: TalerError }
-
-export const ExchangeApiProvider = ({
- baseUrl,
- children,
- frameOnError,
-}: {
- baseUrl: string,
- children: ComponentChildren;
- frameOnError: FunctionComponent<{ children: ComponentChildren }>,
-}): VNode => {
- const [checked, setChecked] = useState<ConfigResult>()
- const { i18n } = useTranslationContext();
- const url = new URL(baseUrl)
- const api = new TalerExchangeHttpClient(url.href, new BrowserFetchHttpLib())
- useEffect(() => {
- api.getConfig()
- .then((resp) => {
- if (api.isCompatible(resp.body.version)) {
- setChecked({ type: "ok", config: resp.body });
- } else {
- setChecked({ type: "incompatible", result: resp.body, supported: api.PROTOCOL_VERSION })
- }
- })
- .catch((error: unknown) => {
- if (error instanceof TalerError) {
- setChecked({ type: "error", error });
- }
- });
- }, []);
-
- if (checked === undefined) {
- return h(frameOnError, { children: h("div", {}, "loading...") })
- }
- if (checked.type === "error") {
- return h(frameOnError, { children: h(ErrorLoading, { error: checked.error, showDetail: true }) })
- }
- if (checked.type === "incompatible") {
- return h(frameOnError, { children: h("div", {}, i18n.str`the bank backend is not supported. supported version "${checked.supported}", server version "${checked.result.version}"`) })
- }
- const value: Type = {
- url, config: checked.config, api
- }
- return h(Context.Provider, {
- value,
- children,
- });
-};
-
diff --git a/packages/aml-backoffice-ui/src/context/ui-forms.ts b/packages/aml-backoffice-ui/src/context/ui-forms.ts
new file mode 100644
index 000000000..3a25234d2
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/context/ui-forms.ts
@@ -0,0 +1,76 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { codecForUIForms, UiForms } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = UiForms;
+
+const defaultForms: UiForms = {
+ forms: [],
+};
+const Context = createContext<Type>(defaultForms);
+
+export type BaseForm = Record<string, unknown>;
+
+export const useUiFormsContext = (): Type => useContext(Context);
+
+export const UiFormsProvider = ({
+ children,
+ value,
+}: {
+ value: UiForms;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
+
+
+function removeUndefineField<T extends object>(obj: T): T {
+ const keys = Object.keys(obj) as Array<keyof T>;
+ return keys.reduce((prev, cur) => {
+ if (typeof prev[cur] === "undefined") {
+ delete prev[cur];
+ }
+ return prev;
+ }, obj);
+}
+
+export function fetchUiForms(listener: (s: UiForms) => void): void {
+ fetch("./forms.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForUIForms().decode(json))
+ .then((result) =>
+ listener({
+ ...defaultForms,
+ ...removeUndefineField(result),
+ }),
+ )
+ .catch((e) => {
+ console.log("failed to fetch forms", e);
+ listener(defaultForms);
+ });
+}
diff --git a/packages/aml-backoffice-ui/src/context/ui-settings.ts b/packages/aml-backoffice-ui/src/context/ui-settings.ts
new file mode 100644
index 000000000..aa318a918
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/context/ui-settings.ts
@@ -0,0 +1,110 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { buildCodecForObject, canonicalizeBaseUrl, Codec, codecForString, codecOptional } from "@gnu-taler/taler-util";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = UiSettings;
+
+/**
+ * Global settings for the UI.
+ */
+const defaultSettings: UiSettings = {
+ backendBaseURL: buildDefaultBackendBaseURL(),
+ signupEmail: undefined,
+};
+
+const Context = createContext<Type>(defaultSettings);
+
+export const useUiSettingsContext = (): Type => useContext(Context);
+
+export const UiSettingsProvider = ({
+ children,
+ value,
+}: {
+ value: UiSettings;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
+export interface UiSettings {
+ // Where libeufin backend is localted
+ // default: window.origin without "webui/"
+ backendBaseURL?: string;
+ // Shows a button "create random account" in the registration form
+ // Useful for testing
+ // default: false
+ signupEmail?: string;
+}
+
+const codecForUISettings = (): Codec<UiSettings> =>
+ buildCodecForObject<UiSettings>()
+ .property("backendBaseURL", codecOptional(codecForString()))
+ .property("signupEmail", codecOptional(codecForString()))
+ .build("UiSettings");
+
+function removeUndefineField<T extends object>(obj: T): T {
+ const keys = Object.keys(obj) as Array<keyof T>;
+ return keys.reduce((prev, cur) => {
+ if (typeof prev[cur] === "undefined") {
+ delete prev[cur];
+ }
+ return prev;
+ }, obj);
+}
+
+export function fetchUiSettings(listener: (s: UiSettings) => void): void {
+ fetch("./settings.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForUISettings().decode(json))
+ .then((result) =>
+ listener({
+ ...defaultSettings,
+ ...removeUndefineField(result),
+ }),
+ )
+ .catch((e) => {
+ console.log("failed to fetch settings", e);
+ listener(defaultSettings);
+ });
+}
+
+function buildDefaultBackendBaseURL(): string | undefined {
+ if (typeof window !== "undefined") {
+ const currentLocation = new URL(
+ window.location.pathname,
+ window.location.origin,
+ ).href;
+ /**
+ * By default, backend serves the html content
+ * from the /webui root.
+ */
+ return canonicalizeBaseUrl(currentLocation.replace("/webui", ""));
+ }
+ throw Error("No default URL");
+}
+
+
diff --git a/packages/aml-backoffice-ui/src/declaration.d.ts b/packages/aml-backoffice-ui/src/declaration.d.ts
index 6af72042c..7868e41bd 100644
--- a/packages/aml-backoffice-ui/src/declaration.d.ts
+++ b/packages/aml-backoffice-ui/src/declaration.d.ts
@@ -1,3 +1,18 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
declare const __VERSION__: string;
declare const __GIT_HASH__: string;
diff --git a/packages/aml-backoffice-ui/src/forms.json b/packages/aml-backoffice-ui/src/forms.json
new file mode 100644
index 000000000..f095e6eb2
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms.json
@@ -0,0 +1,532 @@
+{
+ "forms": [
+ {
+ "label": "Information on customer",
+ "id": "902_1e",
+ "version": 1,
+ "config": {
+ "type": "double-column",
+ "design": [
+ {
+ "title": "Information on customer",
+ "description": "The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.",
+ "fields": [
+ {
+ "type": "choiceStacked",
+
+ "name": "customerType",
+ "id": ".customerType",
+ "label": "Type of customer",
+ "help": "Select one and complete the next form",
+ "required": true,
+ "choices": [
+ {
+ "label": "Natural person",
+ "value": "natural"
+ },
+ {
+ "label": "Legal entity",
+ "value": "legal"
+ }
+ ]
+ },
+ {
+ "type": "group",
+
+ "label": "Natural customer form",
+ "name": "algo",
+ "id": "algo",
+ "before": "a) Country risk (nationality)",
+ "after": "a) Country risk (nationality)",
+ "fields": [
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.fullName",
+ "id": ".naturalCustomer.fullName",
+ "label": "Full name",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.address",
+ "id": ".naturalCustomer.address",
+ "label": "Residential address",
+ "required": true
+ },
+ {
+ "type": "integer",
+
+ "name": "naturalCustomer.telephone",
+ "id": ".naturalCustomer.telephone",
+ "label": "Telephone"
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.email",
+ "id": ".naturalCustomer.email",
+ "label": "E-mail"
+ },
+ {
+ "type": "absoluteTimeText",
+
+ "pattern": "dd/MM/yyyy",
+ "name": "naturalCustomer.dateOfBirth",
+ "id": ".naturalCustomer.dateOfBirth",
+ "label": "Date of birth",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.nationality",
+ "id": ".naturalCustomer.nationality",
+ "label": "Nationality",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.document",
+ "id": ".naturalCustomer.document",
+ "label": "Identification document",
+ "required": true
+ },
+ {
+ "type": "file",
+
+ "name": "naturalCustomer.documentAttachment",
+ "id": ".naturalCustomer.documentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".pdf",
+ "help": "PDF file with max size of 2 mega bytes"
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.companyName",
+ "id": ".naturalCustomer.companyName",
+ "label": "Company name"
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.office",
+ "id": ".naturalCustomer.office",
+ "label": "Registered office"
+ },
+ {
+ "type": "text",
+
+ "name": "naturalCustomer.companyDocument",
+ "id": ".naturalCustomer.companyDocument",
+ "label": "Company identification document"
+ },
+ {
+ "type": "file",
+
+ "name": "naturalCustomer.companyDocumentAttachment",
+ "id": ".naturalCustomer.companyDocumentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".png",
+ "help": "PNG file with max size of 2 mega bytes"
+ }
+ ]
+ },
+
+ {
+ "type": "group",
+
+ "label": "Natural customer form",
+ "name": "algo",
+ "id": "algo",
+ "before": "a) Country risk (nationality)",
+ "after": "a) Country risk (nationality)",
+ "fields": [
+ {
+ "type": "text",
+
+ "name": "legalCustomer.companyName",
+ "id": ".legalCustomer.companyName",
+ "label": "Company name",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "legalCustomer.domicile",
+ "id": ".legalCustomer.domicile",
+ "label": "Domicile",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "legalCustomer.contactPerson",
+ "id": ".legalCustomer.contactPerson",
+ "label": "Contact person"
+ },
+ {
+ "type": "text",
+
+ "name": "legalCustomer.telephone",
+ "id": ".legalCustomer.telephone",
+ "label": "Telephone"
+ },
+ {
+ "type": "text",
+
+ "name": "legalCustomer.email",
+ "id": ".legalCustomer.email",
+ "label": "E-mail"
+ },
+ {
+ "type": "text",
+
+ "name": "legalCustomer.document",
+ "id": ".legalCustomer.document",
+ "label": "Identification document",
+ "help": "Not older than 12 month"
+ },
+ {
+ "type": "file",
+
+ "name": "legalCustomer.documentAttachment",
+ "id": ".legalCustomer.documentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".png",
+ "help": "PNG file with max size of 2 mega bytes"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Information on the natural persons who establish the business relationship for legal entities and partnerships",
+ "description": "For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified.",
+ "fields": [
+ {
+ "type": "array",
+
+ "name": "businessEstablisher",
+ "id": ".businessEstablisher",
+ "label": "Persons",
+ "required": true,
+ "labelFieldId": "fullName",
+ "placeholder": "this is the placeholder",
+ "fields": [
+ {
+ "type": "text",
+
+ "name": "fullName",
+ "id": ".fullName",
+ "label": "Full name",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "address",
+ "id": ".address",
+ "label": "Residential address",
+ "required": true
+ },
+ {
+ "type": "absoluteTimeText",
+
+ "pattern": "dd/MM/yyyy",
+ "name": "dateOfBirth",
+ "id": ".dateOfBirth",
+ "label": "Date of birth",
+ "required": true
+ },
+
+ {
+ "type": "text",
+
+ "name": "nationality",
+ "id": ".nationality",
+ "label": "Nationality",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "typeOfAuthorization",
+ "id": ".typeOfAuthorization",
+ "label": "Type of authorization (signatory of representation)",
+ "required": true
+ },
+ {
+ "type": "file",
+
+ "name": "documentAttachment",
+ "id": ".documentAttachment",
+ "label": "Identification document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".pdf",
+ "help": "PDF file with max size of 2 mega bytes"
+ },
+ {
+ "type": "choiceStacked",
+
+ "name": "powerOfAttorneyArrangements",
+ "id": ".powerOfAttorneyArrangements",
+ "label": "Power of attorney arrangements",
+ "required": true,
+ "choices": [
+ {
+ "label": "CR extract",
+ "value": "cr"
+ },
+ {
+ "label": "Mandate",
+ "value": "mandate"
+ },
+ {
+ "label": "Other",
+ "value": "other"
+ }
+ ]
+ },
+ {
+ "type": "text",
+
+ "name": "powerOfAttorneyArrangementsOther",
+ "id": ".powerOfAttorneyArrangementsOther",
+ "label": "Power of attorney arrangements",
+ "required": true
+ }
+ ],
+ "labelField": "fullName"
+ }
+ ]
+ },
+ {
+ "title": "Acceptance of business relationship",
+ "fields": [
+ {
+ "type": "absoluteTimeText",
+
+ "name": "acceptance.when",
+ "id": ".acceptance.when",
+ "pattern": "dd/MM/yyyy",
+ "converterId": "Taler.AbsoluteTime",
+ "label": "Date (conclusion of contract)"
+ },
+ {
+ "type": "choiceStacked",
+
+ "name": "acceptance.acceptedBy",
+ "id": ".acceptance.acceptedBy",
+ "label": "Accepted by",
+ "required": true,
+ "choices": [
+ {
+ "label": "Face-to-face meeting with customer",
+ "value": "face-to-face"
+ },
+ {
+ "label": "Correspondence: authenticated copy of identification document obtained",
+ "value": "correspondence-document"
+ },
+ {
+ "label": "Correspondence: residential address validated",
+ "value": "correspondence-address"
+ }
+ ]
+ },
+ {
+ "type": "choiceStacked",
+
+ "name": "acceptance.typeOfCorrespondence",
+ "id": ".acceptance.typeOfCorrespondence",
+ "label": "Type of correspondence service",
+ "choices": [
+ {
+ "label": "to the customer",
+ "value": "customer"
+ },
+ {
+ "label": "hold at bank",
+ "value": "bank"
+ },
+ {
+ "label": "to the member",
+ "value": "member"
+ },
+ {
+ "label": "to a third party",
+ "value": "third-party"
+ }
+ ]
+ },
+ {
+ "type": "text",
+
+ "name": "acceptance.thirdPartyFullName",
+ "id": ".acceptance.thirdPartyFullName",
+ "label": "Third party full name",
+ "required": true
+ },
+ {
+ "type": "text",
+
+ "name": "acceptance.thirdPartyAddress",
+ "id": ".acceptance.thirdPartyAddress",
+ "label": "Third party address",
+ "required": true
+ },
+ {
+ "type": "selectMultiple",
+
+ "name": "acceptance.language",
+ "id": ".acceptance.language",
+ "label": "Languages",
+ "choices": [
+ {
+ "label": "Espanol",
+ "value": "es"
+ }
+ ],
+ "unique": true
+ },
+ {
+ "type": "textArea",
+
+ "name": "acceptance.furtherInformation",
+ "id": ".acceptance.furtherInformation",
+ "label": "Further information"
+ }
+ ]
+ },
+ {
+ "title": "Information on the beneficial owner of the assets and/or controlling person",
+ "description": "Establishment of the beneficial owner of the assets and/or controlling person",
+ "fields": [
+ {
+ "type": "choiceStacked",
+
+ "name": "establishment",
+ "id": ".establishment",
+ "label": "The customer is",
+ "required": true,
+ "choices": [
+ {
+ "label": "a natural person and there are no doubts that this person is the sole beneficial owner of the assets",
+ "value": "natural"
+ },
+ {
+ "label": "a foundation (or a similar construct; incl. underlying companies)",
+ "value": "foundation"
+ },
+ {
+ "label": "a trust (incl. underlying companies)",
+ "value": "trust"
+ },
+ {
+ "label": "a life insurance policy with separately managed accounts/securities accounts",
+ "value": "insurance-wrapper"
+ },
+ {
+ "label": "all other cases",
+ "value": "other"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship",
+ "description": "Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)",
+ "fields": [
+ {
+ "type": "textArea",
+
+ "name": "embargoEvaluation",
+ "id": ".embargoEvaluation",
+ "help": "The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.",
+ "label": "Evaluation"
+ }
+ ]
+ },
+ {
+ "title": "In the case of cash transactions/occasional customers: Information on type and purpose of business relationship",
+ "description": "These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created",
+ "fields": [
+ {
+ "type": "choiceStacked",
+
+ "name": "cashTransactions.typeOfBusiness",
+ "id": ".cashTransactions.typeOfBusiness",
+ "label": "Type of business relationship",
+ "choices": [
+ {
+ "label": "Money exchange",
+ "value": "money-exchange"
+ },
+ {
+ "label": "Money and asset transfer",
+ "value": "money-and-asset-transfer"
+ },
+ {
+ "label": "Other cash transactions. Specify below",
+ "value": "other"
+ }
+ ]
+ },
+ {
+ "type": "text",
+
+ "name": "cashTransactions.otherTypeOfBusiness",
+ "id": ".cashTransactions.otherTypeOfBusiness",
+ "required": true,
+ "label": "Specify other cash transactions:"
+ },
+ {
+ "type": "textArea",
+ "name": "cashTransactions.purpose",
+ "id": ".cashTransactions.purpose",
+ "label": "Purpose of the business relationship (purpose of service requested)"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "label": "Example form",
+ "id": "example",
+ "version": 1,
+ "config": {
+ "type": "double-column",
+ "design": [
+ {
+ "title": "Amount inputs",
+ "fields": [
+ {
+ "type": "amount",
+ "name": "thedate",
+ "id": ".amount",
+ "converterId": "Taler.Amount",
+ "help": "how much do you have?",
+ "currency":"EUR",
+ "label": "Amount"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ],
+ "not_yet_supported": []
+}
diff --git a/packages/aml-backoffice-ui/src/forms/902_11e.ts b/packages/aml-backoffice-ui/src/forms/902_11e.ts
index 71ca8bcf4..7cf710741 100644
--- a/packages/aml-backoffice-ui/src/forms/902_11e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_11e.ts
@@ -1,9 +1,23 @@
-import type { TranslatedString } from "@gnu-taler/taler-util";
-import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
-import { BaseForm } from "./declaration.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import type { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_11.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
title:
@@ -13,14 +27,14 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "contractingPartner",
label: i18n.str`Contracting partner`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "declares",
label:
i18n.str`The contracting partner hereby declares that`,
@@ -46,7 +60,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "array",
- props: {
+ properties: {
name: "people",
label: i18n.str`People`,
required: true,
@@ -54,7 +68,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "lastName",
label: i18n.str`Last name(s)`,
required: true,
@@ -62,7 +76,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "firstName",
label: i18n.str`First name(s)`,
required: true,
@@ -70,7 +84,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label: i18n.str`Actual address of domicile`,
required: true,
@@ -82,7 +96,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "fiduciaryAssets",
label: i18n.str`Fiduciary holding assets`,
help: i18n.str`Is a third person the beneficial owner of the assets held in the account/securities account?`,
@@ -103,7 +117,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
],
},
- resolutionSection(current, i18n),
+ resolutionSection(i18n),
],
behavior: function formBehavior(
v: Partial<Form902_11.Form>,
diff --git a/packages/aml-backoffice-ui/src/forms/902_12e.ts b/packages/aml-backoffice-ui/src/forms/902_12e.ts
index 0c08d274c..5aa3f4cf9 100644
--- a/packages/aml-backoffice-ui/src/forms/902_12e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_12e.ts
@@ -1,23 +1,38 @@
-import type { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
-import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import type { AbsoluteTime } from "@gnu-taler/taler-util";
+import type { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-import { BaseForm } from "./declaration.js";
-export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_12.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
title: i18n.str`Foundations`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "contractingPartner",
label: i18n.str`Contracting partner`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "knownAs",
label:
i18n.str`The undersigned hereby declare(s) that as board member of the foundation, or of the highest supervisory body of an underlying company of a foundation, known as`,
@@ -25,7 +40,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "foundation.name",
label:
i18n.str`Name and information pertaining to the foundation`,
@@ -33,7 +48,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "foundation.type",
label: i18n.str`Type of foundation`,
choices: [
@@ -50,7 +65,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "foundation.revocability",
label: i18n.str`Revocability`,
choices: [
@@ -67,7 +82,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "array",
- props: {
+ properties: {
label:
i18n.str`Information pertaining to the (ultimate economic, not fiduciary) founder (individual(s) or entity/ies)`,
labelField: "fullName",
@@ -75,7 +90,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -83,7 +98,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
i18n.str`Actual address of domicile/registered office`,
@@ -91,28 +106,28 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "country",
label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfDeath",
label: i18n.str`Date of death`,
help: i18n.str`if deceased`,
@@ -120,7 +135,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToRevoke",
required: true,
label:
@@ -142,7 +157,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "array",
- props: {
+ properties: {
label:
i18n.str`If the foundation results from the restructuring of pre-existing foundation (re-settlement) or the merger of pre-existing foundations, the following information pertaining to the (actual) founder(s) of the pre-existing foundation(s) has to be given`,
labelField: "fullName",
@@ -150,7 +165,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -158,7 +173,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
i18n.str`Actual address of domicile/registered office`,
@@ -166,28 +181,28 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "country",
label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfDeath",
label: i18n.str`Date of death`,
help: i18n.str`if deceased`,
@@ -198,7 +213,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "array",
- props: {
+ properties: {
label:
i18n.str`Pertaining to the beneficiary/-ies at the time of the signing of this form`,
labelField: "fullName",
@@ -206,7 +221,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -214,7 +229,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
i18n.str`Actual address of domicile/registered office`,
@@ -222,28 +237,28 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "country",
label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToClaim",
label:
i18n.str`Has the beneficiary an actual right to claim distribution?`,
@@ -261,7 +276,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form`,
name: "beneficiaryExtra",
@@ -272,7 +287,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "array",
- props: {
+ properties: {
label:
i18n.str`Information pertaining to further persons having the right to determine or nominate representatives (e.g.) members of the foundation board), if these representatives may dispose over the assets or have the right to change the distribution of the assets or the nomination of beneficiaries`,
labelField: "fullName",
@@ -280,7 +295,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -288,7 +303,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
i18n.str`Actual address of domicile/registered office`,
@@ -296,28 +311,28 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "country",
label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToClaim",
label:
i18n.str`has the person the right to revoke the foundation?`,
@@ -335,7 +350,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the founder) known at the time of the signing of this form`,
name: "beneficiaryExtra",
@@ -346,39 +361,39 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "signature",
label: i18n.str`Signature`,
},
},
],
},
- resolutionSection(current, i18n),
+ resolutionSection(i18n),
],
- behavior: function formBehavior(
- v: Partial<Form902_12.Form>,
- ): FormState<Form902_12.Form> {
- return {
- founders: {
- elements: (v.founders ?? []).map((f) => {
- return {
- rightToRevoke: {
- hidden: v.foundation?.revocability !== "revocable",
- },
- };
- }),
- },
- withRightToNominate: {
- elements: (v.withRightToNominate ?? []).map((f) => {
- return {
- rightToRevoke: {
- hidden: v.foundation?.revocability !== "revocable",
- },
- };
- }),
- },
- };
- },
+ // behavior: function formBehavior(
+ // v: Partial<Form902_12.Form>,
+ // ): FormState<Form902_12.Form> {
+ // return {
+ // founders: {
+ // elements: (v.founders ?? []).map(() => {
+ // return {
+ // rightToRevoke: {
+ // hidden: v.foundation?.revocability !== "revocable",
+ // },
+ // };
+ // }),
+ // },
+ // withRightToNominate: {
+ // elements: (v.withRightToNominate ?? []).map(() => {
+ // return {
+ // rightToRevoke: {
+ // hidden: v.foundation?.revocability !== "revocable",
+ // },
+ // };
+ // }),
+ // },
+ // };
+ // },
});
namespace Form902_12 {
diff --git a/packages/aml-backoffice-ui/src/forms/902_13e.ts b/packages/aml-backoffice-ui/src/forms/902_13e.ts
index f69884e0e..d71266489 100644
--- a/packages/aml-backoffice-ui/src/forms/902_13e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_13e.ts
@@ -1,23 +1,38 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import type { AbsoluteTime } from "@gnu-taler/taler-util";
-import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
-import { BaseForm } from "./declaration.js";
+import type { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_13.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
title: i18n.str`Declaration for trusts`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "contractingPartner",
label: i18n.str`Contracting partner`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "knownAs",
label:
i18n.str`The undersigned hereby declare(s) that as trustee or a member of highest supervisory body of an underlying company of a trust known as`,
@@ -25,7 +40,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "trust.name",
label:
i18n.str`Name and information pertaining to the trust`,
@@ -33,7 +48,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "trust.type",
label: i18n.str`Type of trust`,
choices: [
@@ -50,7 +65,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "trust.revocability",
label: i18n.str`Revocability`,
choices: [
@@ -67,7 +82,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "array",
- props: {
+ properties: {
label:
i18n.str`Information pertaining to the (ultimate economic, not fiduciary) settlor of the trust (individual(s) or entity/ies)`,
labelField: "fullName",
@@ -75,7 +90,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -83,7 +98,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
i18n.str`Actual address of domicile/registered office`,
@@ -91,14 +106,14 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "country",
label: i18n.str`Country`,
},
},
{
type: "absoluteTime",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
@@ -107,14 +122,14 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
},
},
{
type: "absoluteTime",
- props: {
+ properties: {
name: "dateOfDeath",
label: i18n.str`Date of death`,
pattern: "dd/MM/yyyy",
@@ -124,7 +139,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToRevoke",
required: true,
label:
@@ -146,7 +161,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "array",
- props: {
+ properties: {
label:
i18n.str`If the trust results from the restructuring of pre-existing trust (re-settlement) or the merger of pre-existing trusts, the following information pertaining to the (actual) settlor of the pre-existing trust(s) has to be given`,
labelField: "fullName",
@@ -154,7 +169,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -162,7 +177,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
i18n.str`Actual address of domicile/registered office`,
@@ -170,14 +185,14 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "country",
label: i18n.str`Country`,
},
},
{
type: "absoluteTime",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
@@ -186,14 +201,14 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
},
},
{
type: "absoluteTime",
- props: {
+ properties: {
name: "dateOfDeath",
label: i18n.str`Date of death`,
pattern: "dd/MM/yyyy",
@@ -206,7 +221,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "array",
- props: {
+ properties: {
label:
i18n.str`Pertaining to the beneficiary/-ies at the time of the signing of this form`,
labelField: "fullName",
@@ -214,7 +229,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -222,7 +237,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
i18n.str`Actual address of domicile/registered office`,
@@ -230,14 +245,14 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "country",
label: i18n.str`Country`,
},
},
{
type: "absoluteTime",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
@@ -246,14 +261,14 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToClaim",
label:
i18n.str`Has the beneficiary an actual right to claim distribution?`,
@@ -271,7 +286,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`in addition to certain beneficiaries or if there is/are no defined beneficiary/ies pertaining to (a) group(s) of beneficiaries (e.g. descendants of the settlor) known at the time of the signing of this form`,
name: "beneficiaryExtra",
@@ -282,7 +297,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "array",
- props: {
+ properties: {
label:
i18n.str`Information pertaining to the protector(s) as well as (a) further person(s) having the right to revoke the trust (in case of revocable trusts) or to appoint the trustee of a trust`,
labelField: "asd",
@@ -293,7 +308,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
{
type: "array",
- props: {
+ properties: {
label:
i18n.str`Information pertaining to the protectors`,
labelField: "fullName",
@@ -301,7 +316,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -309,7 +324,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
i18n.str`Actual address of domicile/registered office`,
@@ -317,28 +332,28 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "country",
label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToClaim",
label:
i18n.str`Does the protector have the right to revoke the trust?`,
@@ -359,7 +374,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "array",
- props: {
+ properties: {
label:
i18n.str`Information pertaining to further persons`,
labelField: "fullName",
@@ -367,7 +382,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -375,7 +390,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label:
i18n.str`Actual address of domicile/registered office`,
@@ -383,28 +398,28 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "country",
label: i18n.str`Country`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "rightToClaim",
label:
i18n.str`Has this further person the right to revoke the trust?`,
@@ -425,48 +440,48 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "signature",
label: i18n.str`Signature`,
},
},
],
},
- resolutionSection(current, i18n),
+ resolutionSection(i18n),
],
- behavior: function formBehavior(
- v: Partial<Form902_13.Form>,
- ): FormState<Form902_13.Form> {
- return {
- settlors: {
- elements: (v.settlors ?? []).map((f) => {
- return {
- rightToRevoke: {
- hidden: v.foundation?.revocability !== "revocable",
- },
- };
- }),
- },
- protectors: {
- elements: (v.protectors ?? []).map((f) => {
- return {
- rightToRevoke: {
- hidden: v.foundation?.revocability !== "revocable",
- },
- };
- }),
- },
- furtherPersons: {
- elements: (v.furtherPersons ?? []).map((f) => {
- return {
- rightToRevoke: {
- hidden: v.foundation?.revocability !== "revocable",
- },
- };
- }),
- },
- };
- },
+ // behavior: function formBehavior(
+ // v: Partial<Form902_13.Form>,
+ // ): FormState<Form902_13.Form> {
+ // return {
+ // settlors: {
+ // elements: (v.settlors ?? []).map(() => {
+ // return {
+ // rightToRevoke: {
+ // hidden: v.foundation?.revocability !== "revocable",
+ // },
+ // };
+ // }),
+ // },
+ // protectors: {
+ // elements: (v.protectors ?? []).map(() => {
+ // return {
+ // rightToRevoke: {
+ // hidden: v.foundation?.revocability !== "revocable",
+ // },
+ // };
+ // }),
+ // },
+ // furtherPersons: {
+ // elements: (v.furtherPersons ?? []).map(() => {
+ // return {
+ // rightToRevoke: {
+ // hidden: v.foundation?.revocability !== "revocable",
+ // },
+ // };
+ // }),
+ // },
+ // };
+ // },
});
namespace Form902_13 {
diff --git a/packages/aml-backoffice-ui/src/forms/902_15e.ts b/packages/aml-backoffice-ui/src/forms/902_15e.ts
index 2375de389..eeda166c1 100644
--- a/packages/aml-backoffice-ui/src/forms/902_15e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_15e.ts
@@ -1,9 +1,24 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import type { AbsoluteTime } from "@gnu-taler/taler-util";
-import type { FlexibleForm, InternationalizationAPI } from "@gnu-taler/web-util/browser";
-import { BaseForm } from "./declaration.js";
+import type { InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_15.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
title:
@@ -11,14 +26,14 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "contractingPartner",
label: i18n.str`Contracting partner`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "contractualRelationship",
label:
i18n.str`Name or number of the contractual relationship between the contracting party and the financial intermediary`,
@@ -26,33 +41,33 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "insurancePolicy",
label: i18n.str`Insurance policy`,
},
},
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`The contracting partner confirms in accordance with Art. 41a SRO Regulations that it is a licensed and state-supervised insurance company and that it has entered into the above-mentioned contractual relationship the assets connected to the life insurance policy also mentioned above.`,
},
},
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`In relation with the above insurance policy, the contracting partner gives the following further details`,
},
},
{
type: "group",
- props: {
+ properties: {
before: i18n.str`Policy holder`,
fields: [
{
type: "text",
- props: {
+ properties: {
name: "holder.fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -60,7 +75,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "holder.address",
label:
i18n.str`Actual address of domicile/registered office (incl. country)`,
@@ -68,7 +83,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "absoluteTime",
- props: {
+ properties: {
name: "holder.dateOfBirth",
label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
@@ -77,7 +92,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "holder.nationality",
label: i18n.str`Nationality`,
},
@@ -87,13 +102,13 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "group",
- props: {
+ properties: {
before:
i18n.str`Person actually (not in a fiduciary capacity) paying the premiums (to be filled in if not identical with point 1 above)`,
fields: [
{
type: "text",
- props: {
+ properties: {
name: "premiumPayer.fullName",
label:
i18n.str`Last name(s), first name(s)/entity`,
@@ -101,7 +116,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "premiumPayer.address",
label:
i18n.str`Actual address of domicile/registered office (incl. country)`,
@@ -109,7 +124,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "absoluteTime",
- props: {
+ properties: {
name: "premiumPayer.dateOfBirth",
label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
@@ -118,7 +133,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "premiumPayer.nationality",
label: i18n.str`Nationality`,
},
@@ -128,28 +143,28 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`The contracting partner hereby undertakes to automatically inform the financial intermediary of any changes. The contracting partner hereby also declares having been given permission by the above individuals and/or entities to transmit their data to the financial intermediary`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "signature",
label: i18n.str`Signature`,
},
},
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)`,
},
},
],
},
- resolutionSection(current, i18n),
+ resolutionSection(i18n),
],
});
diff --git a/packages/aml-backoffice-ui/src/forms/902_1e.ts b/packages/aml-backoffice-ui/src/forms/902_1e.ts
index 2287db369..58ef7e2e8 100644
--- a/packages/aml-backoffice-ui/src/forms/902_1e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_1e.ts
@@ -1,18 +1,32 @@
-import type { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
-import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
-import { BaseForm, uiForms } from "./declaration.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import type { AbsoluteTime } from "@gnu-taler/taler-util";
+import type { InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_1.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
title: i18n.str`Information on customer`,
- description:
- i18n.str`The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.`,
+ description: i18n.str`The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.`,
fields: [
{
type: "choiceStacked",
- props: {
+ properties: {
name: "customerType",
label: i18n.str`Type of customer`,
required: true,
@@ -30,7 +44,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.fullName",
label: i18n.str`Full name`,
required: true,
@@ -38,7 +52,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.address",
label: i18n.str`Residential address`,
required: true,
@@ -46,21 +60,21 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "integer",
- props: {
+ properties: {
name: "naturalCustomer.telephone",
label: i18n.str`Telephone`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.email",
label: i18n.str`E-mail`,
},
},
{
type: "absoluteTime",
- props: {
+ properties: {
name: "naturalCustomer.dateOfBirth",
label: i18n.str`Date of birth`,
required: true,
@@ -69,7 +83,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.nationality",
label: i18n.str`Nationality`,
required: true,
@@ -77,7 +91,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.document",
label: i18n.str`Identification document`,
required: true,
@@ -85,7 +99,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "file",
- props: {
+ properties: {
name: "naturalCustomer.documentAttachment",
label: i18n.str`Document attachment`,
required: true,
@@ -96,28 +110,28 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.companyName",
label: i18n.str`Company name`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.office",
label: i18n.str`Registered office`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "naturalCustomer.companyDocument",
label: i18n.str`Company identification document`,
},
},
{
type: "file",
- props: {
+ properties: {
name: "naturalCustomer.companyDocumentAttachment",
label: i18n.str`Document attachment`,
required: true,
@@ -128,7 +142,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.companyName",
label: i18n.str`Company name`,
required: true,
@@ -136,7 +150,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.domicile",
label: i18n.str`Domicile`,
required: true,
@@ -144,28 +158,28 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.contactPerson",
label: i18n.str`Contact person`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.telephone",
label: i18n.str`Telephone`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.email",
label: i18n.str`E-mail`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "legalCustomer.document",
label: i18n.str`Identification document`,
help: i18n.str`Not older than 12 month`,
@@ -173,7 +187,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "file",
- props: {
+ properties: {
name: "legalCustomer.documentAttachment",
label: i18n.str`Document attachment`,
required: true,
@@ -185,14 +199,12 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
],
},
{
- title:
- i18n.str`Information on the natural persons who establish the business relationship for legal entities and partnerships`,
- description:
- i18n.str`For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified.`,
+ title: i18n.str`Information on the natural persons who establish the business relationship for legal entities and partnerships`,
+ description: i18n.str`For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified.`,
fields: [
{
type: "array",
- props: {
+ properties: {
name: "businessEstablisher",
label: i18n.str`Persons`,
required: true,
@@ -200,7 +212,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "fullName",
label: i18n.str`Full name`,
required: true,
@@ -208,7 +220,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label: i18n.str`Residential address`,
required: true,
@@ -216,7 +228,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "absoluteTime",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
required: true,
@@ -225,7 +237,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
required: true,
@@ -233,19 +245,17 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "typeOfAuthorization",
- label:
- i18n.str`Type of authorization (signatory of representation)`,
+ label: i18n.str`Type of authorization (signatory of representation)`,
required: true,
},
},
{
type: "file",
- props: {
+ properties: {
name: "documentAttachment",
- label:
- i18n.str`Identification document attachment`,
+ label: i18n.str`Identification document attachment`,
required: true,
maxBites: 2 * 1024 * 1024,
accept: ".png",
@@ -254,7 +264,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "powerOfAttorneyArrangements",
label: i18n.str`Power of attorney arrangements`,
required: true,
@@ -276,7 +286,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "powerOfAttorneyArrangementsOther",
label: i18n.str`Power of attorney arrangements`,
required: true,
@@ -293,7 +303,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "absoluteTime",
- props: {
+ properties: {
name: "acceptance.when",
pattern: "dd/MM/yyyy",
label: i18n.str`Date (conclusion of contract)`,
@@ -302,7 +312,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "acceptance.acceptedBy",
label: i18n.str`Accepted by`,
required: true,
@@ -312,13 +322,11 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
value: "face-to-face",
},
{
- label:
- i18n.str`Correspondence: authenticated copy of identification document obtained`,
+ label: i18n.str`Correspondence: authenticated copy of identification document obtained`,
value: "correspondence-document",
},
{
- label:
- i18n.str`Correspondence: residential address validated`,
+ label: i18n.str`Correspondence: residential address validated`,
value: "correspondence-address",
},
],
@@ -326,7 +334,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
name: "acceptance.typeOfCorrespondence",
label: i18n.str`Type of correspondence service`,
choices: [
@@ -351,7 +359,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "acceptance.thirdPartyFullName",
label: i18n.str`Third party full name`,
required: true,
@@ -359,7 +367,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "acceptance.thirdPartyAddress",
label: i18n.str`Third party address`,
required: true,
@@ -367,16 +375,16 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "selectMultiple",
- props: {
+ properties: {
name: "acceptance.language",
label: i18n.str`Languages`,
- choices: uiForms.currencies(i18n),
+ choices: ["asd"],
unique: true,
},
},
{
type: "textArea",
- props: {
+ properties: {
name: "acceptance.furtherInformation",
label: i18n.str`Further information`,
},
@@ -384,36 +392,30 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
],
},
{
- title:
- i18n.str`Information on the beneficial owner of the assets and/or controlling person`,
- description:
- i18n.str`Establishment of the beneficial owner of the assets and/or controlling person`,
+ title: i18n.str`Information on the beneficial owner of the assets and/or controlling person`,
+ description: i18n.str`Establishment of the beneficial owner of the assets and/or controlling person`,
fields: [
{
type: "choiceStacked",
- props: {
+ properties: {
name: "establishment",
label: i18n.str`The customer is`,
required: true,
choices: [
{
- label:
- i18n.str`a natural person and there are no doubts that this person is the sole beneficial owner of the assets`,
+ label: i18n.str`a natural person and there are no doubts that this person is the sole beneficial owner of the assets`,
value: "natural",
},
{
- label:
- i18n.str`a foundation (or a similar construct; incl. underlying companies)`,
+ label: i18n.str`a foundation (or a similar construct; incl. underlying companies)`,
value: "foundation",
},
{
- label:
- i18n.str`a trust (incl. underlying companies)`,
+ label: i18n.str`a trust (incl. underlying companies)`,
value: "trust",
},
{
- label:
- i18n.str`a life insurance policy with separately managed accounts/securities accounts`,
+ label: i18n.str`a life insurance policy with separately managed accounts/securities accounts`,
value: "insurance-wrapper",
},
{
@@ -426,14 +428,12 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
],
},
{
- title:
- i18n.str`Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship`,
- description:
- i18n.str`Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)`,
+ title: i18n.str`Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship`,
+ description: i18n.str`Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)`,
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "embargoEvaluation",
help: i18n.str`The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.`,
label: i18n.str`Evaluation`,
@@ -442,14 +442,12 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
],
},
{
- title:
- i18n.str`In the case of cash transactions/occasional customers: Information on type and purpose of business relationship`,
- description:
- i18n.str`These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created`,
+ title: i18n.str`In the case of cash transactions/occasional customers: Information on type and purpose of business relationship`,
+ description: i18n.str`These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created`,
fields: [
{
type: "choiceStacked",
- props: {
+ properties: {
name: "cashTransactions.typeOfBusiness",
label: i18n.str`Type of business relationship`,
choices: [
@@ -462,8 +460,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
value: "money-and-asset-transfer",
},
{
- label:
- i18n.str`Other cash transactions. Specify below`,
+ label: i18n.str`Other cash transactions. Specify below`,
value: "other",
},
],
@@ -471,7 +468,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "cashTransactions.otherTypeOfBusiness",
required: true,
label: i18n.str`Specify other cash transactions:`,
@@ -479,108 +476,107 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
name: "cashTransactions.purpose",
- label:
- i18n.str`Purpose of the business relationship (purpose of service requested)`,
+ label: i18n.str`Purpose of the business relationship (purpose of service requested)`,
},
},
],
},
- resolutionSection(current, i18n),
+ resolutionSection(i18n),
],
- behavior: function formBehavior(
- v: Partial<Form902_1.Form>,
- ): FormState<Form902_1.Form> {
- return {
- fullName: {
- disabled: true,
- },
- businessEstablisher: {
- elements: (v.businessEstablisher ?? []).map((be) => {
- return {
- powerOfAttorneyArrangementsOther: {
- hidden: be.powerOfAttorneyArrangements !== "other",
- },
- };
- }),
- },
- acceptance: {
- thirdPartyFullName: {
- hidden: v.acceptance?.typeOfCorrespondence !== "third-party",
- },
- thirdPartyAddress: {
- hidden: v.acceptance?.typeOfCorrespondence !== "third-party",
- },
- },
- cashTransactions: {
- otherTypeOfBusiness: {
- hidden: v.cashTransactions?.typeOfBusiness !== "other",
- },
- },
- naturalCustomer: {
- fullName: {
- hidden: v.customerType !== "natural",
- },
- address: {
- hidden: v.customerType !== "natural",
- },
- telephone: {
- hidden: v.customerType !== "natural",
- },
- email: {
- hidden: v.customerType !== "natural",
- },
- dateOfBirth: {
- hidden: v.customerType !== "natural",
- },
- nationality: {
- hidden: v.customerType !== "natural",
- },
- document: {
- hidden: v.customerType !== "natural",
- },
- companyName: {
- hidden: v.customerType !== "natural",
- },
- office: {
- hidden: v.customerType !== "natural",
- },
- companyDocument: {
- hidden: v.customerType !== "natural",
- },
- companyDocumentAttachment: {
- hidden: v.customerType !== "natural",
- },
- documentAttachment: {
- hidden: v.customerType !== "natural",
- },
- },
- legalCustomer: {
- companyName: {
- hidden: v.customerType !== "legal",
- },
- contactPerson: {
- hidden: v.customerType !== "legal",
- },
- document: {
- hidden: v.customerType !== "legal",
- },
- domicile: {
- hidden: v.customerType !== "legal",
- },
- email: {
- hidden: v.customerType !== "legal",
- },
- telephone: {
- hidden: v.customerType !== "legal",
- },
- documentAttachment: {
- hidden: v.customerType !== "legal",
- },
- },
- };
- },
+ // behavior: function formBehavior(
+ // v: Partial<Form902_1.Form>,
+ // ): FormState<Form902_1.Form> {
+ // return {
+ // fullName: {
+ // disabled: true,
+ // },
+ // businessEstablisher: {
+ // elements: (v.businessEstablisher ?? []).map((be) => {
+ // return {
+ // powerOfAttorneyArrangementsOther: {
+ // hidden: be.powerOfAttorneyArrangements !== "other",
+ // },
+ // };
+ // }),
+ // },
+ // acceptance: {
+ // thirdPartyFullName: {
+ // hidden: v.acceptance?.typeOfCorrespondence !== "third-party",
+ // },
+ // thirdPartyAddress: {
+ // hidden: v.acceptance?.typeOfCorrespondence !== "third-party",
+ // },
+ // },
+ // cashTransactions: {
+ // otherTypeOfBusiness: {
+ // hidden: v.cashTransactions?.typeOfBusiness !== "other",
+ // },
+ // },
+ // naturalCustomer: {
+ // fullName: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // address: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // telephone: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // email: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // dateOfBirth: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // nationality: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // document: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // companyName: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // office: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // companyDocument: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // companyDocumentAttachment: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // documentAttachment: {
+ // hidden: v.customerType !== "natural",
+ // },
+ // },
+ // legalCustomer: {
+ // companyName: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // contactPerson: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // document: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // domicile: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // email: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // telephone: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // documentAttachment: {
+ // hidden: v.customerType !== "legal",
+ // },
+ // },
+ // };
+ // },
});
namespace Form902_1 {
@@ -632,11 +628,11 @@ namespace Form902_1 {
interface BeneficialOwner {
establishment:
- | "natural-person"
- | "foundation"
- | "trust"
- | "insurance-wrapper"
- | "other";
+ | "natural-person"
+ | "foundation"
+ | "trust"
+ | "insurance-wrapper"
+ | "other";
}
interface CashTransactions {
diff --git a/packages/aml-backoffice-ui/src/forms/902_4e.ts b/packages/aml-backoffice-ui/src/forms/902_4e.ts
index b31a8dcba..7a3af8731 100644
--- a/packages/aml-backoffice-ui/src/forms/902_4e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_4e.ts
@@ -1,11 +1,26 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import type { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
-import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import type { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
import { h as create } from "preact";
-import { resolutionSection } from "./simplest.js";
-import { BaseForm } from "./declaration.js";
+import { BaseForm } from "../context/ui-forms.js";
import { ArrowRightIcon, ChevronRightIcon } from "./icons.js";
+import { resolutionSection } from "./simplest.js";
-export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_4.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
title: i18n.str`Risk Profile AMLA`,
@@ -14,7 +29,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`The member performs additional clarifications if the business relationship or the transaction is classified as increased risk (Art. 56 SRO Regulations)`,
before: create(ArrowRightIcon, { class: "h-6 w-6" }),
@@ -22,7 +37,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "customer",
label: i18n.str`Customer`,
help: i18n.str`Pursuant identification form (VQF doc. Nr. 902.1) numeral 1`,
@@ -36,7 +51,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`This evaluation has to be completed by all members for every business relationship`,
before: create(ArrowRightIcon, { class: "h-6 w-6" }),
@@ -44,7 +59,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Foreign PEP`,
// tooltip:
// i18n.str`Definition see Art. 7 lit. g numeral 1 SRO Regulations`,
@@ -66,7 +81,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
label:
i18n.str`Domestic PEP and PEP of International Organizations`,
// tooltip:
@@ -95,7 +110,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "absoluteTime",
- props: {
+ properties: {
label:
i18n.str`The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on`,
name: "pep.when",
@@ -111,7 +126,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`This evaluation has to be completed by all members for every business relationship`,
before: create(ArrowRightIcon, { class: "h-6 w-6" }),
@@ -119,7 +134,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: '"High risk" or non-cooperative country' as TranslatedString,
help: 'Is the customer, the beneficial owner or the controlling person or authorized representative in a country considered by the FATF "high risk" or non-cooperative and for which FATF requires increased diligence?' as TranslatedString,
name: "highRisk.evaluation",
@@ -139,7 +154,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "absoluteTime",
- props: {
+ properties: {
label:
i18n.str`The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on`,
name: "highRisk.when",
@@ -154,7 +169,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`This evaluation has to be completed by all members who have in total more than 20 customers for every business relationship. At least two risk categories have to be chosen and assessed`,
before: create(ArrowRightIcon, { class: "h-6 w-6" }),
@@ -162,12 +177,12 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "group",
- props: {
+ properties: {
before: i18n.str`a) Country risk (nationality)`,
fields: [
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Domicile/residential address`,
name: "evaluation.nationality.address",
choices: [
@@ -189,7 +204,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Nationality`,
name: "evaluation.nationality.nationality",
choices: [
@@ -207,7 +222,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Risk level`,
name: "evaluation.nationality.risk",
choices: [
@@ -234,12 +249,12 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "group",
- props: {
+ properties: {
before: i18n.str`b) Country risk (business activity)`,
fields: [
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Place of business activity`,
name: "evaluation.business.place",
choices: [
@@ -257,7 +272,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Risk level`,
name: "evaluation.business.risk",
choices: [
@@ -284,19 +299,19 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "group",
- props: {
+ properties: {
before: i18n.str`c) Country risk (payments)`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`Country of origin and destination of frequent payments (if known)`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Risk level`,
name: "evaluation.payments.risk",
choices: [
@@ -323,12 +338,12 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "group",
- props: {
+ properties: {
before: i18n.str`d) Industry risk`,
fields: [
{
type: "choiceStacked",
- props: {
+ properties: {
label:
i18n.str`Nature of customer's business activity`,
name: "evaluation.industry.nature",
@@ -347,7 +362,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Risk level`,
name: "evaluation.payments.risk",
choices: [
@@ -384,19 +399,19 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "group",
- props: {
+ properties: {
before: i18n.str`e) Contact risk`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`Types of contact to the customer/ beneficial owner of the assets`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Risk level`,
name: "evaluation.contact.risk",
choices: [
@@ -423,19 +438,19 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "group",
- props: {
+ properties: {
before: i18n.str`f) Product risk`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`Nature of services and products requested by the customer`,
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Risk level`,
name: "evaluation.product.risk",
choices: [
@@ -482,19 +497,19 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "group",
- props: {
+ properties: {
before: i18n.str`g) Criteria defined by the member`,
fields: [
{
type: "text",
- props: {
+ properties: {
label: i18n.str`Criteria definition`,
name: "evaluation.custom.definition",
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Risk level`,
name: "evaluation.custom.risk",
choices: [
@@ -518,20 +533,20 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`Overall assessment of the business relationship`,
},
},
{
type: "group",
- props: {
+ properties: {
before:
i18n.str`A business relationship is classified as increased risk if:`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`Business relationship with PEP pursuant to numeral 1 (no exception possible)`,
before: create(ChevronRightIcon, { class: "h-6 w-6" }),
@@ -539,7 +554,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
label:
'Relationship with a person from a "high risk" or non-cooperative country according to numeral 2 (no exceptions possible)' as TranslatedString,
before: create(ChevronRightIcon, { class: "h-6 w-6" }),
@@ -547,7 +562,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`Min. one criterion pursuant to numeral 3 was assessed with risk 2 or min. two criteria pursuant to numeral 3 were assessed with risk 1 (exception: justification by the member below why the business relationship overall does not have to be classified as increased risk despite the fact that individual risk criteria are increased)`,
before: create(ChevronRightIcon, { class: "h-6 w-6" }),
@@ -558,7 +573,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`Justification for differing risk assessment`,
name: "evaluation.overall.justification",
@@ -566,7 +581,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Risk classified`,
name: "evaluation.overall.risk",
choices: [
@@ -585,7 +600,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "absoluteTime",
- props: {
+ properties: {
label:
i18n.str`The decision of the Senior executive body on the acceptance of a business relationship with a PEP was obtained on`,
name: "evaluation.when",
@@ -601,19 +616,19 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "group",
- props: {
+ properties: {
before: i18n.str`Criteria`,
fields: [
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`Classification as as increased risk is compulsory if`,
},
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-6 h-6" }),
label:
i18n.str`Transactions for which assets with an equivalent value of CHF 100'000.- or more are physically introduced at the beginning of the business relationship, either at once or in a staggered manner`,
@@ -621,7 +636,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-6 h-6" }),
label:
'Money and asset transfers ("money transfer") whereby a single transaction or multiple transactions which appear to be related reach or exceed the amount of CHF 5,000.-' as TranslatedString,
@@ -629,7 +644,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-6 h-6" }),
label:
'Payments from or to a country that is considered to be "high risk" or non-cooperative by the FATF and for which increased diligence is required' as TranslatedString,
@@ -640,13 +655,13 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "group",
- props: {
+ properties: {
before:
i18n.str`Additional criteria defined by the member`,
fields: [
{
type: "caption",
- props: {
+ properties: {
before: create(ArrowRightIcon, { class: "w-6 h-6" }),
label:
i18n.str`All members have to define min. 1 additional criterion for every business relationship to identify unusual transactions`,
@@ -654,20 +669,20 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label: i18n.str`Description`,
name: "criteria.additional",
},
},
{
type: "group",
- props: {
+ properties: {
before:
i18n.str`Possible criteria (Art. 59 para. 2 SRO Regulations)`,
fields: [
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-4 h-4" }),
label:
i18n.str`the amount of inflowing and outflowing assets`,
@@ -675,7 +690,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-4 h-4" }),
label:
i18n.str`type, volume and frequency of transactions usual to the business relationship (considerable variance would be unusual)`,
@@ -683,7 +698,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-4 h-4" }),
label:
i18n.str`type, volume and frequency of transactions usual to comparable business relationships (considerable variance would be unusual)`,
@@ -691,7 +706,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-4 h-4" }),
label:
i18n.str`description of expected transaction patterns which the client notify the member of (considerable variance would be unusual)`,
@@ -699,7 +714,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
before: create(ChevronRightIcon, { class: "w-4 h-4" }),
label:
'The country of origin or destination of payments, especially in the case of payments from or to a country considered by the FATF as "high risk" or non-cooperative' as TranslatedString,
@@ -713,10 +728,10 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
],
},
- resolutionSection(current, i18n),
+ resolutionSection(i18n),
],
behavior: function formBehavior(
- v: Partial<Form902_4.Form>,
+ // v: Partial<Form902_4.Form>,
): FormState<Form902_4.Form> {
return {
};
diff --git a/packages/aml-backoffice-ui/src/forms/902_5e.ts b/packages/aml-backoffice-ui/src/forms/902_5e.ts
index 3af03ed22..e66a4f94d 100644
--- a/packages/aml-backoffice-ui/src/forms/902_5e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_5e.ts
@@ -1,9 +1,23 @@
-import type { TranslatedString } from "@gnu-taler/taler-util";
-import type { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
-import { BaseForm, uiForms } from "./declaration.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import type { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_5.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) => ({
design: [
{
title: i18n.str`Customer Profile`,
@@ -12,7 +26,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
name: "customer",
label: i18n.str`Customer`,
help: i18n.str`Pursuant Identification Form (VQF doc. No. 902.1) numeral 1`,
@@ -25,7 +39,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "textArea",
- props: {
+ properties: {
label: i18n.str`Profession, business activities`,
name: "businessActivity",
help: i18n.str`former, current, potentially planned`,
@@ -38,7 +52,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "textArea",
- props: {
+ properties: {
label: i18n.str`Income and assets, liabilities`,
name: "financial",
help: i18n.str`estimated`,
@@ -51,7 +65,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "text",
- props: {
+ properties: {
label: i18n.str`Nature`,
name: "originOfAssets.nature",
help: i18n.str`nature of the involved assets`,
@@ -59,22 +73,22 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "selectOne",
- props: {
+ properties: {
name: "originOfAssets.currency",
label: i18n.str`Currency`,
- choices: uiForms.currencies(i18n),
+ choices: ["change me"],
},
},
{
type: "integer",
- props: {
+ properties: {
label: i18n.str`Amount`,
name: "originOfAssets.amount",
},
},
{
type: "choiceStacked",
- props: {
+ properties: {
label: i18n.str`Category`,
name: "originOfAssets.category",
choices: [
@@ -99,7 +113,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
label: i18n.str`Other category`,
name: "originOfAssets.categoryOther",
required: true,
@@ -107,7 +121,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`Detailed description of the origins/economical background of the assets involved in the business relationship`,
name: "originOfAssets.details",
@@ -121,7 +135,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "textArea",
- props: {
+ properties: {
label: i18n.str`Purpose of the business relationship`,
name: "nature.purpose",
help: i18n.str`nature of the involved assets`,
@@ -129,7 +143,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`Information on the planned development of the business relationship and the assets`,
name: "nature.plan",
@@ -137,7 +151,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`Especially in the case of cash or money and asset transfer transactions with regular customers: Details on usual business volume, Information on the beneficiaries, (Full name, address, bank account)`,
name: "nature.cashOrMoneyTransfer",
@@ -150,7 +164,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`Relation of the customer to the beneficial owner involved in the business relationship`,
name: "relations.beneficialOwners",
@@ -158,7 +172,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`Relation of the customer to the controlling persons involved in the business relationship`,
name: "relations.controllingPersons",
@@ -166,7 +180,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`Relation of the customer to the authorized signatories involved in the business relationship`,
name: "relations.authorizedSignatories",
@@ -174,7 +188,7 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label:
i18n.str`Relation of the customer to other persons involved in the business relationship`,
name: "relations.otherPersons",
@@ -182,14 +196,14 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "textArea",
- props: {
+ properties: {
label: i18n.str`Relation to other AMLA-Files`,
name: "relations.withOtherAmlaFiles",
},
},
{
type: "textArea",
- props: {
+ properties: {
label: i18n.str`Introducer / agents / references`,
name: "relations.references",
},
@@ -201,26 +215,26 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "textArea",
- props: {
+ properties: {
label: i18n.str`Other relevant information`,
name: "furtherInformation",
},
},
],
},
- resolutionSection(current, i18n),
+ resolutionSection(i18n),
],
- behavior: function formBehavior(
- v: Partial<Form902_5.Form>,
- ): FormState<Form902_5.Form> {
- return {
- originOfAssets: {
- categoryOther: {
- hidden: v.originOfAssets?.category !== "other",
- },
- },
- };
- },
+ // behavior: function formBehavior(
+ // v: Partial<Form902_5.Form>,
+ // ): FormState<Form902_5.Form> {
+ // return {
+ // originOfAssets: {
+ // categoryOther: {
+ // hidden: v.originOfAssets?.category !== "other",
+ // },
+ // },
+ // };
+ // },
});
namespace Form902_5 {
diff --git a/packages/aml-backoffice-ui/src/forms/902_9e.ts b/packages/aml-backoffice-ui/src/forms/902_9e.ts
index e0e7a6d65..297ec86b1 100644
--- a/packages/aml-backoffice-ui/src/forms/902_9e.ts
+++ b/packages/aml-backoffice-ui/src/forms/902_9e.ts
@@ -1,9 +1,24 @@
-import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
-import { FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import { FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { BaseForm } from "../context/ui-forms.js";
import { resolutionSection } from "./simplest.js";
-import { BaseForm } from "./declaration.js";
-export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Form902_9.Form> => ({
+export const v1 = (i18n: InternationalizationAPI) =>({
design: [
{
title:
@@ -11,42 +26,42 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
fields: [
{
type: "textArea",
- props: {
+ properties: {
name: "contractingPartner",
label: i18n.str`Contracting partner`,
},
},
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`The contracting partner hereby declares that the person(s) listed below is/are the beneficial owner(s) of the assets involved in the business relationship. If the contracting partner is also the sole beneficial owner of the assets, the contracting partner's detail must be set out below`,
},
},
{
type: "array",
- props: {
+ properties: {
label: i18n.str`Persons`,
labelField: "surname",
name: "persons",
fields: [
{
type: "text",
- props: {
+ properties: {
name: "surname",
label: i18n.str`Surname(s)`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "firstName",
label: i18n.str`First name(s)`,
},
},
{
type: "absoluteTime",
- props: {
+ properties: {
name: "dateOfBirth",
label: i18n.str`Date of birth`,
pattern: "dd/MM/yyyy",
@@ -55,14 +70,14 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "text",
- props: {
+ properties: {
name: "nationality",
label: i18n.str`Nationality`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "address",
label: i18n.str`Actual address of domicile`,
},
@@ -72,31 +87,31 @@ export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): Flexib
},
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`The contracting partner hereby undertakes to inform automatically of any changes to the information contained herein`,
},
},
{
type: "text",
- props: {
+ properties: {
name: "signature",
label: i18n.str`Signature`,
},
},
{
type: "caption",
- props: {
+ properties: {
label:
i18n.str`It is a criminal offense to deliberately provide false information on this form (article 251 of the Swiss Criminal Code, document forgery)`,
},
},
],
},
- resolutionSection(current, i18n),
+ resolutionSection(i18n),
],
behavior: function formBehavior(
- v: Partial<Form902_9.Form>,
+ // v: Partial<Form902_9.Form>,
): FormState<Form902_9.Form> {
return {
};
diff --git a/packages/aml-backoffice-ui/src/forms/declaration.ts b/packages/aml-backoffice-ui/src/forms/declaration.ts
deleted file mode 100644
index ec3bc5189..000000000
--- a/packages/aml-backoffice-ui/src/forms/declaration.ts
+++ /dev/null
@@ -1,70 +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 type { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
-import type { FlexibleForm, InternationalizationAPI } from "@gnu-taler/web-util/browser";
-import { AmlExchangeBackend } from "../utils/types.js";
-
-/**
- * import entry point without hard reference.
- *
- * This file just export types and UI Forms
- * based on what `globalThis` contains.
- *
- * `./index.js` must be imported first before
- * so `globaThis` will have the correct value.
- */
-
-export interface BaseForm {
- state: AmlExchangeBackend.AmlState;
- threshold: AmountJson;
-}
-
-export type FormMetadata<T extends BaseForm> = {
- label: TranslatedString,
- id: string,
- version: number,
- impl: (current: T) => FlexibleForm<T>
-}
-
-interface LabelValue {
- label: TranslatedString;
- value: string,
-}
-
-export interface UiForms {
- currencies: (i18n: InternationalizationAPI) => LabelValue[],
- languages: (i18n: InternationalizationAPI) => LabelValue[],
- forms: (i18n: InternationalizationAPI) => Array<FormMetadata<BaseForm>>,
-}
-
-/**
- * Global settings for the UI.
- */
-const defaultUIForms: UiForms = {
- currencies: () => [],
- languages: () => [],
- forms: () => [],
-};
-
-declare global {
- var amlExchangeBackoffice: UiForms;
-}
-
-export const uiForms: UiForms =
- "amlExchangeBackoffice" in globalThis
- ? (globalThis as any).amlExchangeBackoffice
- : defaultUIForms;
diff --git a/packages/aml-backoffice-ui/src/forms/icons.tsx b/packages/aml-backoffice-ui/src/forms/icons.tsx
index 392790c9c..8bd369c4f 100644
--- a/packages/aml-backoffice-ui/src/forms/icons.tsx
+++ b/packages/aml-backoffice-ui/src/forms/icons.tsx
@@ -1,3 +1,18 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import { h } from "preact";
export const ChevronRightIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
diff --git a/packages/aml-backoffice-ui/src/forms/index.ts b/packages/aml-backoffice-ui/src/forms/index.ts
index f41122bc7..e89a8fb10 100644
--- a/packages/aml-backoffice-ui/src/forms/index.ts
+++ b/packages/aml-backoffice-ui/src/forms/index.ts
@@ -1,14 +1,20 @@
-import type { InternationalizationAPI } from "@gnu-taler/web-util/browser";
-import { v1 as form_902_11e_v1 } from "./902_11e.js";
-import { v1 as form_902_12e_v1 } from "./902_12e.js";
-import { v1 as form_902_13e_v1 } from "./902_13e.js";
-import { v1 as form_902_15e_v1 } from "./902_15e.js";
-import { v1 as form_902_1e_v1 } from "./902_1e.js";
-import { v1 as form_902_4e_v1 } from "./902_4e.js";
-import { v1 as form_902_5e_v1 } from "./902_5e.js";
-import { v1 as form_902_9e_v1 } from "./902_9e.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import type { FormMetadata, InternationalizationAPI } from "@gnu-taler/web-util/browser";
import { v1 as simplest } from "./simplest.js";
-import { BaseForm, FormMetadata } from "./declaration.js";
const languages = (i18n: InternationalizationAPI) => [
{
@@ -122,52 +128,52 @@ const languages = (i18n: InternationalizationAPI) => [
];
-const forms: (i18n: InternationalizationAPI) => Array<FormMetadata<BaseForm>> = (i18n) => [
+export const preloadedForms: (i18n: InternationalizationAPI) => Array<FormMetadata> = (i18n) => [
{
label: i18n.str`Simple comment`,
- id: "simple_comment",
- version: 1,
- impl: simplest(i18n),
- }, {
- label: i18n.str`Identification form`,
- id: "902.1e",
- version: 1,
- impl: form_902_1e_v1(i18n),
- }, {
- label: i18n.str`Operational legal entity or partnership`,
- id: "902.11e",
- version: 1,
- impl: form_902_11e_v1(i18n),
- }, {
- label: i18n.str`Foundations`,
- id: "902.12e",
- version: 1,
- impl: form_902_12e_v1(i18n),
- }, {
- label: i18n.str`Declaration for trusts`,
- id: "902.13e",
- version: 1,
- impl: form_902_13e_v1(i18n),
- }, {
- label: i18n.str`Information on life insurance policies`,
- id: "902.15e",
- version: 1,
- impl: form_902_15e_v1(i18n),
- }, {
- label: i18n.str`Declaration of beneficial owner`,
- id: "902.9e",
- version: 1,
- impl: form_902_9e_v1(i18n),
- }, {
- label: i18n.str`Customer profile`,
- id: "902.5e",
- version: 1,
- impl: form_902_5e_v1(i18n),
- }, {
- label: i18n.str`Risk profile`,
- id: "902.4e",
+ id: "__simple_comment",
version: 1,
- impl: form_902_4e_v1(i18n),
+ config: simplest(i18n),
+ // }, {
+ // label: i18n.str`Identification form`,
+ // id: "902.1e",
+ // version: 1,
+ // config: form_902_1e_v1(i18n),
+ // }, {
+ // label: i18n.str`Operational legal entity or partnership`,
+ // id: "902.11e",
+ // version: 1,
+ // config: form_902_11e_v1(i18n),
+ // }, {
+ // label: i18n.str`Foundations`,
+ // id: "902.12e",
+ // version: 1,
+ // config: form_902_12e_v1(i18n),
+ // }, {
+ // label: i18n.str`Declaration for trusts`,
+ // id: "902.13e",
+ // version: 1,
+ // config: form_902_13e_v1(i18n),
+ // }, {
+ // label: i18n.str`Information on life insurance policies`,
+ // id: "902.15e",
+ // version: 1,
+ // config: form_902_15e_v1(i18n),
+ // }, {
+ // label: i18n.str`Declaration of beneficial owner`,
+ // id: "902.9e",
+ // version: 1,
+ // config: form_902_9e_v1(i18n),
+ // }, {
+ // label: i18n.str`Customer profile`,
+ // id: "902.5e",
+ // version: 1,
+ // config: form_902_5e_v1(i18n),
+ // }, {
+ // label: i18n.str`Risk profile`,
+ // id: "902.4e",
+ // version: 1,
+ // config: form_902_4e_v1(i18n),
},
];
@@ -199,4 +205,3 @@ const currencies = (i18n: InternationalizationAPI) => [
},
];
-globalThis.amlExchangeBackoffice = { currencies, languages, forms }
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
index 735ca9bfc..4cd781b74 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -1,84 +1,90 @@
-import type {
- TranslatedString
-} from "@gnu-taler/taler-util";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
-import type { DoubleColumnFormSection, FlexibleForm, FormState, InternationalizationAPI } from "@gnu-taler/web-util/browser";
-import { BaseForm } from "./declaration.js";
-import { amlStateConverter } from "../utils/converter.js";
-import { AmlExchangeBackend } from "../utils/types.js";
+import type {
+ DoubleColumnForm,
+ DoubleColumnFormSection,
+ InternationalizationAPI,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
-export const v1 = (i18n: InternationalizationAPI) => (current: BaseForm): FlexibleForm<Simplest.Form> => ({
+export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({
+ type: "double-column" as const,
design: [
{
title: i18n.str`Simple form`,
fields: [
{
type: "textArea",
- props: {
- name: "comment",
- label: i18n.str`Comments`,
- },
+ id: ".comment" as UIHandlerId,
+ name: "comment",
+ label: i18n.str`Comment`,
},
],
},
- resolutionSection(current, i18n),
+ resolutionSection(i18n),
],
- behavior: function formBehavior(
- v: Partial<Simplest.Form>,
- ): FormState<Simplest.Form> {
- return {
- comment: {
- help: ((v.comment?.length ?? 0) > 100 ? "keep it short" : "") as TranslatedString,
-
- },
- threshold: {
- disabled: v.state === AmlExchangeBackend.AmlState.frozen,
- },
- };
- },
+ // behavior: function formBehavior(
+ // v: Partial<Simplest.Form>,
+ // ): FormState<Simplest.Form> {
+ // return {
+ // comment: {
+ // help: ((v.comment?.length ?? 0) > 100 ? "keep it short" : "") as TranslatedString,
+ // },
+ // threshold: {
+ // disabled: v.state === TalerExchangeApi.AmlState.frozen,
+ // },
+ // };
+ // },
});
-export namespace Simplest {
- export interface Form extends BaseForm {
- comment: string;
- }
-}
-
-export function resolutionSection(current: BaseForm, i18n: InternationalizationAPI): DoubleColumnFormSection {
+export function resolutionSection(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormSection {
return {
title: i18n.str`Resolution`,
- description: `Current state is ${amlStateConverter.toStringUI(
- current.state,
- )} and threshold at ` as TranslatedString,
fields: [
{
type: "choiceHorizontal",
- props: {
- name: "state",
- label: i18n.str`New state`,
- converter: amlStateConverter,
- choices: [
- {
- value: AmlExchangeBackend.AmlState.frozen,
- label: i18n.str`Frozen`,
- },
- {
- value: AmlExchangeBackend.AmlState.pending,
- label: i18n.str`Pending`,
- },
- {
- value: AmlExchangeBackend.AmlState.normal,
- label: i18n.str`Normal`,
- },
- ],
- },
+ id: ".state" as UIHandlerId,
+ name: "state",
+ label: i18n.str`New state`,
+ converterId: "TalerExchangeApi.AmlState",
+ choices: [
+ {
+ value: "frozen",
+ label: i18n.str`Frozen`,
+ },
+ {
+ value: "pending",
+ label: i18n.str`Pending`,
+ },
+ {
+ value: "normal",
+ label: i18n.str`Normal`,
+ },
+ ],
},
{
type: "amount",
- props: {
- name: "threshold",
- label: i18n.str`New threshold`,
- },
+ id: ".threshold" as UIHandlerId,
+ currency: "NETZBON",
+ name: "threshold",
+ converterId: "Taler.Amount",
+ label: i18n.str`New threshold`,
},
],
};
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts
new file mode 100644
index 000000000..70b2db571
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -0,0 +1,227 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ TalerExchangeApi,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ UIFieldHandler,
+ UIFormElementConfig,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+import { undefinedIfEmpty } from "../pages/CreateAccount.js";
+
+// export type UIField = {
+// value: string | undefined;
+// onUpdate: (s: string) => void;
+// error: TranslatedString | undefined;
+// };
+
+export type FormHandler<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? UIFieldHandler
+ : T[k] extends AmountJson
+ ? UIFieldHandler
+ : T[k] extends TalerExchangeApi.AmlState
+ ? UIFieldHandler
+ : FormHandler<T[k]>;
+};
+
+export type FormValues<T> = {
+ [k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>;
+};
+
+export type RecursivePartial<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? string
+ : T[k] extends AmountJson
+ ? AmountJson
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TalerExchangeApi.AmlState
+ : RecursivePartial<T[k]>;
+};
+
+export type FormErrors<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? TranslatedString
+ : T[k] extends AmountJson
+ ? TranslatedString
+ : T[k] extends AbsoluteTime
+ ? TranslatedString
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TranslatedString
+ : FormErrors<T[k]>;
+};
+
+export type FormStatus<T> =
+ | {
+ status: "ok";
+ result: T;
+ errors: undefined;
+ }
+ | {
+ status: "fail";
+ result: RecursivePartial<T>;
+ errors: FormErrors<T>;
+ };
+
+function constructFormHandler<T>(
+ shape: Array<UIHandlerId>,
+ form: RecursivePartial<FormValues<T>>,
+ updateForm: (d: RecursivePartial<FormValues<T>>) => void,
+ errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+ const handler = shape.reduce((handleForm, fieldId) => {
+ const path = fieldId.split(".");
+
+ function updater(newValue: unknown) {
+ updateForm(setValueDeeper(form, path, newValue));
+ }
+
+ const currentValue = getValueDeeper<string>(form as any, path, undefined);
+ const currentError = getValueDeeper<TranslatedString>(
+ errors as any,
+ path,
+ undefined,
+ );
+ const field: UIFieldHandler = {
+ error: currentError,
+ value: currentValue,
+ onChange: updater,
+ state: {}, //FIXME: add the state of the field (hidden, )
+ };
+
+ return setValueDeeper(handleForm, path, field);
+ }, {} as FormHandler<T>);
+
+ return handler;
+}
+
+/**
+ * FIXME: Consider sending this to web-utils
+ *
+ *
+ * @param defaultValue
+ * @param check
+ * @returns
+ */
+export function useFormState<T>(
+ shape: Array<UIHandlerId>,
+ defaultValue: RecursivePartial<FormValues<T>>,
+ check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
+): [FormHandler<T>, FormStatus<T>] {
+ const [form, updateForm] =
+ useState<RecursivePartial<FormValues<T>>>(defaultValue);
+
+ const status = check(form);
+ const handler = constructFormHandler(shape, form, updateForm, status.errors);
+
+ return [handler, status];
+}
+
+interface Tree<T> extends Record<string, Tree<T> | T> {}
+
+export function getValueDeeper<T>(
+ object: Tree<T> | undefined,
+ names: string[],
+ notFoundValue?: T,
+): T | undefined {
+ if (names.length === 0) return object as T;
+ const [head, ...rest] = names;
+ if (!head) {
+ return getValueDeeper(object, rest, notFoundValue);
+ }
+ if (object === undefined) {
+ return notFoundValue;
+ }
+ return getValueDeeper(object[head] as Tree<T>, rest, notFoundValue);
+}
+
+export function setValueDeeper(object: any, names: string[], value: any): any {
+ if (names.length === 0) return value;
+ const [head, ...rest] = names;
+ if (!head) {
+ return setValueDeeper(object, rest, value);
+ }
+ if (object === undefined) {
+ return undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) });
+ }
+ return undefinedIfEmpty({ ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) });
+}
+
+export function getShapeFromFields(
+ fields: UIFormElementConfig[],
+): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(
+ shape,
+ getShapeFromFields(field.fields),
+ );
+ }
+ });
+ return shape;
+}
+
+export function getRequiredFields(
+ fields: UIFormElementConfig[],
+): Array<UIHandlerId> {
+ const shape: Array<UIHandlerId> = [];
+ fields.forEach((field) => {
+ if ("id" in field) {
+ // FIXME: this should be a validation when loading the form
+ // consistency check
+ if (shape.indexOf(field.id) !== -1) {
+ throw Error(`already present: ${field.id}`);
+ }
+ if (!field.required) {
+ return;
+ }
+ shape.push(field.id);
+ } else if (field.type === "group") {
+ Array.prototype.push.apply(
+ shape,
+ getRequiredFields(field.fields),
+ );
+ }
+ });
+ return shape;
+}
+export function validateRequiredFields<FormType>(
+ errors: FormErrors<FormType> | undefined,
+ form: object,
+ fields: Array<UIHandlerId>,
+): FormErrors<FormType> | undefined {
+ let result: FormErrors<FormType> | undefined = errors;
+ fields.forEach((f) => {
+ const path = f.split(".");
+ const v = getValueDeeper(form as any, path);
+ result = setValueDeeper(result, path, !v ? "required" : undefined);
+ });
+ return result;
+}
diff --git a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts b/packages/aml-backoffice-ui/src/hooks/officer.ts
index fe989f3eb..1bb73b8fc 100644
--- a/packages/aml-backoffice-ui/src/hooks/useOfficer.ts
+++ b/packages/aml-backoffice-ui/src/hooks/officer.ts
@@ -1,26 +1,38 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import {
AbsoluteTime,
Codec,
LockedAccount,
OfficerAccount,
OfficerId,
+ OperationOk,
SigningKey,
buildCodecForObject,
codecForAbsoluteTime,
codecForString,
- codecOptional,
createNewOfficerAccount,
decodeCrock,
encodeCrock,
+ opFixedSuccess,
unlockOfficerAccount,
} from "@gnu-taler/taler-util";
-import {
- buildStorageKey,
- useLocalStorage,
- useMemoryStorage,
-} from "@gnu-taler/web-util/browser";
+import { buildStorageKey, useExchangeApiContext, useLocalStorage } from "@gnu-taler/web-util/browser";
import { useMemo } from "preact/hooks";
-import { useExchangeApiContext, useMaybeExchangeApiContext } from "../context/config.js";
+import { usePreferences } from "./preferences.js";
export interface Officer {
account: LockedAccount;
@@ -30,9 +42,9 @@ export interface Officer {
const codecForLockedAccount = codecForString() as Codec<LockedAccount>;
type OfficerAccountString = {
- id: string,
+ id: string;
strKey: string;
-}
+};
export const codecForOfficerAccount = (): Codec<OfficerAccountString> =>
buildCodecForObject<OfficerAccountString>()
@@ -50,62 +62,68 @@ export type OfficerState = OfficerNotReady | OfficerReady;
export type OfficerNotReady = OfficerNotFound | OfficerLocked;
interface OfficerNotFound {
state: "not-found";
- create: (password: string) => Promise<void>;
+ create: (password: string) => Promise<OperationOk<OfficerId>>;
}
interface OfficerLocked {
state: "locked";
- forget: () => void;
- tryUnlock: (password: string) => Promise<void>;
+ forget: () => OperationOk<void>;
+ tryUnlock: (password: string) => Promise<OperationOk<void>>;
}
interface OfficerReady {
state: "ready";
account: OfficerAccount;
- forget: () => void;
- lock: () => void;
+ forget: () => OperationOk<void>;
+ lock: () => OperationOk<void>;
}
const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
-const DEV_ACCOUNT_KEY = buildStorageKey("account-dev", codecForOfficerAccount());
-const ACCOUNT_KEY = "account";
+const DEV_ACCOUNT_KEY = buildStorageKey(
+ "account-dev",
+ codecForOfficerAccount(),
+);
export function useOfficer(): OfficerState {
- const exchangeContext = useMaybeExchangeApiContext();
- // dev account, is save when reloaded.
+ const {lib:{exchange: api}} = useExchangeApiContext();
+ const [pref] = usePreferences();
+ pref.keepSessionAfterReload;
+ // dev account, is kept on reloaded.
const accountStorage = useLocalStorage(DEV_ACCOUNT_KEY);
const account = useMemo(() => {
- if (!accountStorage.value) return undefined
+ if (!accountStorage.value) return undefined;
+
return {
id: accountStorage.value.id as OfficerId,
- signingKey: decodeCrock(accountStorage.value.strKey) as SigningKey
- }
- }, [accountStorage.value])
-
-
- // const accountStorage = useMemoryStorage<OfficerAccount>(ACCOUNT_KEY);
- // const account = accountStorage.value;
+ signingKey: decodeCrock(accountStorage.value.strKey) as SigningKey,
+ };
+ }, [accountStorage.value?.id, accountStorage.value?.strKey]);
const officerStorage = useLocalStorage(OFFICER_KEY);
- const officer = officerStorage.value;
+ const officer = useMemo(() => {
+ if (!officerStorage.value) return undefined;
+ return officerStorage.value;
+ }, [officerStorage.value?.account, officerStorage.value?.when.t_ms]);
if (officer === undefined) {
return {
state: "not-found",
create: async (pwd: string) => {
- if (!exchangeContext) return;
- const req = await fetch(new URL("seed", exchangeContext.api.baseUrl).href)
- const b = await req.blob()
- const ar = await b.arrayBuffer()
- const uintar = new Uint8Array(ar)
+ const resp = await api.getSeed()
+ const extraEntropy = resp.type === "ok" ? resp.body : new Uint8Array();
- const { id, safe, signingKey } = await createNewOfficerAccount(pwd, uintar);
+ const { id, safe, signingKey } = await createNewOfficerAccount(
+ pwd,
+ extraEntropy,
+ );
officerStorage.update({
account: safe,
when: AbsoluteTime.now(),
});
// accountStorage.update({ id, signingKey });
- const strKey = encodeCrock(signingKey)
- accountStorage.update({ id, strKey })
+ const strKey = encodeCrock(signingKey);
+ accountStorage.update({ id, strKey });
+
+ return opFixedSuccess(id)
},
};
}
@@ -115,11 +133,16 @@ export function useOfficer(): OfficerState {
state: "locked",
forget: () => {
officerStorage.reset();
+ return opFixedSuccess(undefined)
},
tryUnlock: async (pwd: string) => {
const ac = await unlockOfficerAccount(officer.account, pwd);
// accountStorage.update(ac);
- accountStorage.update({ id: ac.id, strKey: encodeCrock(ac.signingKey) })
+ accountStorage.update({
+ id: ac.id,
+ strKey: encodeCrock(ac.signingKey),
+ });
+ return opFixedSuccess(undefined)
},
};
}
@@ -129,10 +152,12 @@ export function useOfficer(): OfficerState {
account,
lock: () => {
accountStorage.reset();
+ return opFixedSuccess(undefined)
},
forget: () => {
officerStorage.reset();
accountStorage.reset();
+ return opFixedSuccess(undefined)
},
};
}
diff --git a/packages/aml-backoffice-ui/src/hooks/useSettings.ts b/packages/aml-backoffice-ui/src/hooks/preferences.ts
index f1610576e..12e85d249 100644
--- a/packages/aml-backoffice-ui/src/hooks/useSettings.ts
+++ b/packages/aml-backoffice-ui/src/hooks/preferences.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -18,57 +18,68 @@ import {
Codec,
TranslatedString,
buildCodecForObject,
- codecForBoolean,
- codecForNumber,
- codecForString,
- codecOptional
+ codecForBoolean
} from "@gnu-taler/taler-util";
-import { buildStorageKey, useLocalStorage, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
-interface Settings {
+interface Preferences {
allowInsecurePassword: boolean;
keepSessionAfterReload: boolean;
}
-export function getAllBooleanSettings(): Array<keyof Settings> {
- return ["allowInsecurePassword", "keepSessionAfterReload"]
-}
-
-export function getLabelForSetting(k: keyof Settings, i18n: ReturnType<typeof useTranslationContext>["i18n"]): TranslatedString {
- switch (k) {
- case "allowInsecurePassword": return i18n.str`Allow Insecure password`
- case "keepSessionAfterReload": return i18n.str`Keep session after reload`
- }
-}
-
-export const codecForSettings = (): Codec<Settings> =>
- buildCodecForObject<Settings>()
+export const codecForPreferences = (): Codec<Preferences> =>
+ buildCodecForObject<Preferences>()
.property("allowInsecurePassword", (codecForBoolean()))
.property("keepSessionAfterReload", (codecForBoolean()))
- .build("Settings");
+ .build("Preferences");
-const defaultSettings: Settings = {
+const defaultPreferences: Preferences = {
allowInsecurePassword: false,
keepSessionAfterReload: false,
};
-const EXCHANGE_SETTINGS_KEY = buildStorageKey(
- "exchange-settings",
- codecForSettings(),
+const PREFERENCES_KEY = buildStorageKey(
+ "exchange-preferences",
+ codecForPreferences(),
);
-
-export function useSettings(): [
- Readonly<Settings>,
- <T extends keyof Settings>(key: T, value: Settings[T]) => void,
+/**
+ * User preferences.
+ *
+ * @returns tuple of [state, update()]
+ */
+export function usePreferences(): [
+ Readonly<Preferences>,
+ <T extends keyof Preferences>(key: T, value: Preferences[T]) => void,
] {
const { value, update } = useLocalStorage(
- EXCHANGE_SETTINGS_KEY,
- defaultSettings,
+ PREFERENCES_KEY,
+ defaultPreferences,
);
- function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
+ function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) {
const newValue = { ...value, [k]: v };
update(newValue);
}
return [value, updateField];
}
+
+export function getAllBooleanPreferences(): Array<keyof Preferences> {
+ return [
+ "allowInsecurePassword",
+ "keepSessionAfterReload",
+ ];
+}
+
+export function getLabelForPreferences(
+ k: keyof Preferences,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): TranslatedString {
+ switch (k) {
+ case "allowInsecurePassword": return i18n.str`Allow Insecure password`
+ case "keepSessionAfterReload": return i18n.str`Keep session after reload`
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/hooks/useBackend.ts b/packages/aml-backoffice-ui/src/hooks/useBackend.ts
deleted file mode 100644
index 0615c9c99..000000000
--- a/packages/aml-backoffice-ui/src/hooks/useBackend.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
-import {
- HttpResponseOk,
- RequestOptions,
- useApiContext,
-} from "@gnu-taler/web-util/browser";
-import { useCallback } from "preact/hooks";
-import { uiSettings } from "../settings.js";
-
-interface useBackendType {
- request: <T>(
- path: string,
- options?: RequestOptions,
- ) => Promise<HttpResponseOk<T>>;
- fetcher: <T>(args: [string, string]) => Promise<HttpResponseOk<T>>;
- paginatedFetcher: <T>(
- args: [string, number, number, string],
- ) => Promise<HttpResponseOk<T>>;
-}
-export function usePublicBackend(): useBackendType {
- const { request: requestHandler } = useApiContext();
-
- const baseUrl = getInitialBackendBaseURL();
-
- const request = useCallback(
- function requestImpl<T>(
- path: string,
- options: RequestOptions = {},
- ): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, path, options);
- },
- [baseUrl],
- );
-
- const fetcher = useCallback(
- function fetcherImpl<T>([endpoint, talerAmlOfficerSignature]: [string, string]): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, {
- talerAmlOfficerSignature
- });
- },
- [baseUrl],
- );
- const paginatedFetcher = useCallback(
- function fetcherImpl<T>([endpoint, page, size, talerAmlOfficerSignature]: [
- string,
- number,
- number,
- string,
- ]): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, {
- params: { page: page || 1, size },
- talerAmlOfficerSignature,
- });
- },
- [baseUrl],
- );
- return {
- request,
- fetcher,
- paginatedFetcher,
- };
-}
-
-export function getInitialBackendBaseURL(): string {
- const overrideUrl =
- typeof localStorage !== "undefined"
- ? localStorage.getItem("exchange-base-url")
- : undefined;
-
- let result: string;
-
- if (!overrideUrl) {
- //normal path
- if (!uiSettings.backendBaseURL) {
- console.error(
- "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
- );
- result = typeof (window as any) !== "undefined" ? window.origin : "localhost"
- } else {
- result = uiSettings.backendBaseURL;
- }
- } else {
- // testing/development path
- result = overrideUrl
- }
- try {
- return canonicalizeBaseUrl(result)
- } catch (e) {
- //fall back
- return canonicalizeBaseUrl(window.origin)
- }
-}
diff --git a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
index dbc6763ba..78574ada4 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCaseDetails.ts
@@ -1,16 +1,30 @@
-
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { OfficerAccount, PaytoString, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { AmountString, OfficerAccount, PaytoString, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
import _useSWR, { SWRHook } from "swr";
-import { useExchangeApiContext } from "../context/config.js";
-import { useOfficer } from "./useOfficer.js";
+import { useOfficer } from "./officer.js";
+import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
const useSWR = _useSWR as unknown as SWRHook;
export function useCaseDetails(paytoHash: string) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
- const { api } = useExchangeApiContext();
+ const { lib: {exchange: api} } = useExchangeApiContext();
async function fetcher([officer, account]: [OfficerAccount, PaytoString]) {
return await api.getDecisionDetails(officer, account)
@@ -34,62 +48,4 @@ export function useCaseDetails(paytoHash: string) {
return undefined;
}
-const example1: TalerExchangeApi.AmlDecisionDetails = {
- aml_history: [
- {
- justification: "Lack of documentation",
- decider_pub: "ASDASDASD",
- decision_time: {
- t_s: Date.now() / 1000,
- },
- new_state: 2,
- new_threshold: "USD:0" as AmountString,
- },
- {
- justification: "Doing a transfer of high amount",
- decider_pub: "ASDASDASD",
- decision_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 6,
- },
- new_state: 1,
- new_threshold: "USD:2000" as AmountString,
- },
- {
- justification: "Account is known to the system",
- decider_pub: "ASDASDASD",
- decision_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 9,
- },
- new_state: 0,
- new_threshold: "USD:100" as AmountString,
- },
- ],
- kyc_attributes: [
- {
- collection_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 8,
- },
- expiration_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 4,
- },
- provider_section: "asdasd",
- attributes: {
- name: "Sebastian",
- },
- },
- {
- collection_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 5,
- },
- expiration_time: {
- t_s: Date.now() / 1000 - 60 * 60 * 24 * 30 * 2,
- },
- provider_section: "asdasd",
- attributes: {
- creditCard: "12312312312",
- },
- },
- ],
-};
-
diff --git a/packages/aml-backoffice-ui/src/hooks/useCases.ts b/packages/aml-backoffice-ui/src/hooks/useCases.ts
index 68deb7db9..d3a1c1018 100644
--- a/packages/aml-backoffice-ui/src/hooks/useCases.ts
+++ b/packages/aml-backoffice-ui/src/hooks/useCases.ts
@@ -1,95 +1,115 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import { useState } from "preact/hooks";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { AmountString, HttpStatusCode, OfficerAccount, OperationFail, TalerExchangeApi, TalerExchangeResultByMethod, TalerHttpError } from "@gnu-taler/taler-util";
+import {
+ OfficerAccount,
+ OperationOk,
+ TalerExchangeApi,
+ TalerExchangeResultByMethod,
+ TalerHttpError,
+} from "@gnu-taler/taler-util";
import _useSWR, { SWRHook } from "swr";
-import { useExchangeApiContext } from "../context/config.js";
-import { useOfficer } from "./useOfficer.js";
-import { AmlExchangeBackend } from "../utils/types.js";
+import { useOfficer } from "./officer.js";
+import { useExchangeApiContext } from "@gnu-taler/web-util/browser";
const useSWR = _useSWR as unknown as SWRHook;
-const PAGE_SIZE = 10;
+export const PAGINATED_LIST_SIZE = 10;
+// when doing paginated request, ask for one more
+// and use it to know if there are more to request
+export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
+
/**
* FIXME: mutate result when balance change (transaction )
* @param account
* @param args
* @returns
*/
-export function useCases(state: AmlExchangeBackend.AmlState) {
+export function useCases(state: TalerExchangeApi.AmlState) {
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
- const { api } = useExchangeApiContext();
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
const [offset, setOffset] = useState<string>();
- async function fetcher([officer, state, offset]: [OfficerAccount, AmlExchangeBackend.AmlState, string | undefined]) {
+ async function fetcher([officer, state, offset]: [
+ OfficerAccount,
+ TalerExchangeApi.AmlState,
+ string | undefined,
+ ]) {
return await api.getDecisionsByState(officer, state, {
- order: "asc", offset, limit: PAGE_SIZE + 1
- })
+ order: "asc",
+ offset,
+ limit: PAGINATED_LIST_REQUEST,
+ });
}
- const { data, error } = useSWR<TalerExchangeResultByMethod<"getDecisionsByState">, TalerHttpError>(
- !session ? undefined : [session, state, offset],
+ const { data, error } = useSWR<
+ TalerExchangeResultByMethod<"getDecisionsByState">,
+ TalerHttpError
+ >(
+ !session ? undefined : [session, state, offset, "getDecisionsByState"],
fetcher,
);
- // const [lastAfter, setLastAfter] = useState<
- // HttpResponse<AmlExchangeBackend.AmlRecords, AmlExchangeBackend.AmlError>
- // >({ loading: true });
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
- // useEffect(() => {
- // if (afterData) setLastAfter(afterData);
- // }, [afterData]);
+ return buildPaginatedResult(data.body.records, offset, setOffset, (d) =>
+ String(d.rowid),
+ );
+}
- // if (afterError) {
- // return afterError.cause;
- // }
+type PaginatedResult<T> = OperationOk<T> & {
+ isLastPage: boolean;
+ isFirstPage: boolean;
+ loadNext(): void;
+ loadFirst(): void;
+};
- // if the query returns less that we ask, then we have reach the end or beginning
- const isLastPage =
- data && data.type === "ok" && data.body.records.length <= PAGE_SIZE;
- const isFirstPage = !offset;
+//TODO: consider sending this to web-util
+export function buildPaginatedResult<R, OffId>(
+ data: R[],
+ offset: OffId | undefined,
+ setOffset: (o: OffId | undefined) => void,
+ getId: (r: R) => OffId,
+): PaginatedResult<R[]> {
+ const isLastPage = data.length < PAGINATED_LIST_REQUEST;
+ const isFirstPage = offset === undefined;
- const pagination = {
+ const result = structuredClone(data);
+ if (result.length == PAGINATED_LIST_REQUEST) {
+ result.pop();
+ }
+ return {
+ type: "ok",
+ body: result,
isLastPage,
isFirstPage,
- loadMore: () => {
- if (isLastPage || data?.type !== "ok") return;
- const list = data.body.records
- setOffset(String(list[list.length - 1].rowid));
+ loadNext: () => {
+ if (!result.length) return;
+ const id = getId(result[result.length - 1]);
+ setOffset(id);
},
- reset: () => {
- setOffset(undefined)
+ loadFirst: () => {
+ setOffset(undefined);
},
};
-
- // const public_accountslist = data?.type !== "ok" ? [] : data.body.public_accounts;
- if (!session) {
- return {
- data: {
- type: "fail",
- case: HttpStatusCode.Unauthorized,
- detail: {}
- } as OperationFail<never>
- }
- }
-
- if (data) {
- if (data.type === "fail") {
- return { data }
- }
- const records = isLastPage ? data.body.records : removeLastElement(data.body.records)
- return { data: { type: "ok" as const, body: { records } }, pagination }
- }
- if (error) {
- return error;
- }
- return undefined;
}
-
-function removeLastElement<T>(list: Array<T>): Array<T> {
- if (list.length === 0) {
- return list;
- }
- return list.slice(0, -1)
-} \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/i18n/bank.pot b/packages/aml-backoffice-ui/src/i18n/bank.pot
index 66e98976f..39f9de5ce 100644
--- a/packages/aml-backoffice-ui/src/i18n/bank.pot
+++ b/packages/aml-backoffice-ui/src/i18n/bank.pot
@@ -1,5 +1,5 @@
# This file is part of GNU Taler
-# (C) 2022 Taler Systems S.A.
+# (C) 2022-2024 Taler Systems S.A.
#
# GNU Taler is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
diff --git a/packages/aml-backoffice-ui/src/i18n/fr.po b/packages/aml-backoffice-ui/src/i18n/fr.po
index 203d55343..8148f6a0c 100644
--- a/packages/aml-backoffice-ui/src/i18n/fr.po
+++ b/packages/aml-backoffice-ui/src/i18n/fr.po
@@ -1,5 +1,5 @@
# This file is part of GNU Taler
-# (C) 2022 Taler Systems S.A.
+# (C) 2022-2024 Taler Systems S.A.
#
# GNU Taler is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
diff --git a/packages/aml-backoffice-ui/src/i18n/poheader b/packages/aml-backoffice-ui/src/i18n/poheader
index a251e9584..d7a371934 100644
--- a/packages/aml-backoffice-ui/src/i18n/poheader
+++ b/packages/aml-backoffice-ui/src/i18n/poheader
@@ -1,5 +1,5 @@
# This file is part of GNU Taler
-# (C) 2022 Taler Systems S.A.
+# (C) 2022-2024 Taler Systems S.A.
#
# GNU Taler is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
diff --git a/packages/aml-backoffice-ui/src/i18n/strings-prelude b/packages/aml-backoffice-ui/src/i18n/strings-prelude
index a0aeb8268..3ab0fd1e5 100644
--- a/packages/aml-backoffice-ui/src/i18n/strings-prelude
+++ b/packages/aml-backoffice-ui/src/i18n/strings-prelude
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/aml-backoffice-ui/src/i18n/strings.ts b/packages/aml-backoffice-ui/src/i18n/strings.ts
index a779bbc49..4f7419eb4 100644
--- a/packages/aml-backoffice-ui/src/i18n/strings.ts
+++ b/packages/aml-backoffice-ui/src/i18n/strings.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/aml-backoffice-ui/src/index.html b/packages/aml-backoffice-ui/src/index.html
index c1de73520..0ed2f8178 100644
--- a/packages/aml-backoffice-ui/src/index.html
+++ b/packages/aml-backoffice-ui/src/index.html
@@ -1,6 +1,6 @@
<!--
This file is part of GNU Taler
- (C) 2021--2022 Taler Systems S.A.
+ (C) 2021--2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -30,7 +30,6 @@
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<title>Exchange Backoffice</title>
<!-- Entry point for the SPA. -->
- <script type="module" src="forms.js"></script>
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
diff --git a/packages/aml-backoffice-ui/src/index.tsx b/packages/aml-backoffice-ui/src/index.tsx
index c2ac4c84b..c6f6b4a8f 100644
--- a/packages/aml-backoffice-ui/src/index.tsx
+++ b/packages/aml-backoffice-ui/src/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -18,5 +18,8 @@ import { App } from "./App.js";
import { h, render } from "preact";
const app = document.getElementById("app");
-
-render(<App />, app as any);
+if (!app) {
+ console.error("could not found app element")
+} else {
+ render(<App />, app);
+}
diff --git a/packages/aml-backoffice-ui/src/pages.ts b/packages/aml-backoffice-ui/src/pages.ts
deleted file mode 100644
index 109cd31d0..000000000
--- a/packages/aml-backoffice-ui/src/pages.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { AntiMoneyLaunderingForm } from "./pages/AntiMoneyLaunderingForm.js";
-import { CaseDetails } from "./pages/CaseDetails.js";
-import { Cases, HomeIcon, PeopleIcon } from "./pages/Cases.js";
-import { NewFormEntry } from "./pages/NewFormEntry.js";
-import { Officer } from "./pages/Officer.js";
-import { PageEntry, pageDefinition } from "./route.js";
-// import homeLogo from "./assets/home.svg";
-// import peopleLogo from "./assets/people.svg";
-const cases: PageEntry = {
- url: "#/cases",
- view: Cases,
- name: "Cases" as TranslatedString,
- Icon: HomeIcon,
-};
-
-const officer: PageEntry = {
- url: "#/officer",
- view: Officer,
- name: "Officer" as TranslatedString,
- Icon: PeopleIcon,
-};
-
-const account: PageEntry<{ account: string }> = {
- url: pageDefinition("#/account/:account"),
- view: CaseDetails,
- name: "Account" as TranslatedString,
- // icon: () => undefined,
-};
-
-const newFormEntry: PageEntry<{ account?: string; type?: string }> = {
- url: pageDefinition("#/account/:account/new/:type?"),
- view: NewFormEntry,
- name: "New Form" as TranslatedString,
- // icon: () => undefined,
-};
-
-
-export const Pages = {
- cases,
- officer,
- account,
- newFormEntry,
-};
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx
deleted file mode 100644
index 0b055f682..000000000
--- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.stories.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import {
- AntiMoneyLaunderingForm as TestedComponent,
-} from "./AntiMoneyLaunderingForm.js";
-
-export default {
- title: "aml form",
-};
-
-export const SimpleComment = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "simple_comment",
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
-export const Identification = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.1e",
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
-export const OperationalLegalEntity = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.11e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const Foundations = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.12e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const DelcarationOfTrusts = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.13e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
-export const InformationOnLifeInsurance = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.15e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const DeclarationOfBeneficialOwner = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.9e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const CustomerProfile = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.5e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-export const RiskProfile = tests.createExample(TestedComponent, {
- account: "the_account",
- formId: "902.4e",
-
- onSubmit: async (justification, newState, newThreshold) => {
- alert(JSON.stringify({ justification, newState, newThreshold }, undefined, 2))
- }
-});
-
diff --git a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx b/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
deleted file mode 100644
index c42b1e7af..000000000
--- a/packages/aml-backoffice-ui/src/pages/AntiMoneyLaunderingForm.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-import {
- AbsoluteTime,
- AmountJson,
- Amounts,
- Codec,
- OperationFail,
- OperationOk,
- OperationResult,
- buildCodecForObject,
- codecForNumber,
- codecForString,
- codecOptional,
-} from "@gnu-taler/taler-util";
-import {
- DefaultForm,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { h } from "preact";
-import { useExchangeApiContext } from "../context/config.js";
-import { FormMetadata, uiForms } from "../forms/declaration.js";
-import { Pages } from "../pages.js";
-import { AmlExchangeBackend } from "../utils/types.js";
-
-export function AntiMoneyLaunderingForm({
- account,
- formId,
- onSubmit,
-}: {
- account: string;
- formId: string;
- onSubmit: (
- justification: Justification,
- state: AmlExchangeBackend.AmlState,
- threshold: AmountJson,
- ) => Promise<void>;
-}) {
- const { i18n } = useTranslationContext();
- const theForm = uiForms.forms(i18n).find((v) => v.id === formId);
- if (!theForm) {
- return <div>form with id {formId} not found</div>;
- }
-
- const { config } = useExchangeApiContext();
-
- const initial = {
- when: AbsoluteTime.now(),
- state: AmlExchangeBackend.AmlState.pending,
- threshold: Amounts.zeroOfCurrency(config.currency),
- };
- return (
- <DefaultForm
- initial={initial}
- form={theForm.impl(initial)}
- onUpdate={() => { }}
- onSubmit={(formValue) => {
- if (formValue.state === undefined || formValue.threshold === undefined)
- return;
- const st = formValue.state;
- const amount = formValue.threshold;
-
- const justification: Justification = {
- id: theForm.id,
- label: theForm.label,
- version: theForm.version,
- value: formValue,
- };
-
- onSubmit(justification, st, amount);
- }}
- >
- <div class="mt-6 flex items-center justify-end gap-x-6">
- <a
- href={Pages.account.url({ account })}
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- >
- <i18n.Translate>Confirm</i18n.Translate>
- </button>
- </div>
- </DefaultForm>
- );
-}
-
-export type Justification<T = any> = {
- // form values
- value: T;
-} & Omit<Omit<FormMetadata<any>, "icon">, "impl">;
-
-export function stringifyJustification(j: Justification): string {
- return JSON.stringify(j);
-}
-
-type SimpleFormMetadata = {
- version?: number;
- id?: string;
-};
-
-export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> =>
- buildCodecForObject<SimpleFormMetadata>()
- .property("id", codecOptional(codecForString()))
- .property("version", codecOptional(codecForNumber()))
- .build("SimpleFormMetadata");
-
-type ParseJustificationFail =
- | "not-json"
- | "id-not-found"
- | "form-not-found"
- | "version-not-found";
-
-export function parseJustification(
- s: string,
- listOfAllKnownForms: FormMetadata<any>[],
-): OperationOk<{ justification: Justification; metadata: FormMetadata<any> }> | OperationFail<ParseJustificationFail> {
- try {
- const justification = JSON.parse(s);
- const info = codecForSimpleFormMetadata().decode(justification);
- if (!info.id) {
- return {
- type: "fail",
- case: "id-not-found",
- detail: {} as any,
- };
- }
- if (!info.version) {
- return {
- type: "fail",
- case: "version-not-found",
- detail: {} as any,
- };
- }
- const found = listOfAllKnownForms.find((f) => {
- return f.id === info.id && f.version === info.version;
- });
- if (!found) {
- return {
- type: "fail",
- case: "form-not-found",
- detail: {} as any,
- };
- }
- return {
- type: "ok",
- body: {
- justification,
- metadata: found,
- },
- };
- } catch (e) {
- return {
- type: "fail",
- case: "not-json",
- detail: {} as any,
- };
- }
-}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index 0875f047b..bb936cebf 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -1,41 +1,75 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import {
AbsoluteTime,
AmountJson,
Amounts,
+ Codec,
HttpStatusCode,
+ OperationFail,
+ OperationOk,
TalerError,
+ TalerErrorDetail,
+ TalerExchangeApi,
TranslatedString,
- assertUnreachable
+ assertUnreachable,
+ buildCodecForObject,
+ codecForNumber,
+ codecForString,
+ codecOptional,
} from "@gnu-taler/taler-util";
-import { DefaultForm, ErrorLoading, InternationalizationAPI, Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ DefaultForm,
+ ErrorLoading,
+ FormMetadata,
+ InternationalizationAPI,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { Fragment, VNode, h } from "preact";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { FormMetadata } from "../forms/declaration.js";
+import { privatePages } from "../Routing.js";
+import { useUiFormsContext } from "../context/ui-forms.js";
+import { preloadedForms } from "../forms/index.js";
import { useCaseDetails } from "../hooks/useCaseDetails.js";
-import { Pages } from "../pages.js";
-import { Justification, parseJustification } from "./AntiMoneyLaunderingForm.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
-import { AmlExchangeBackend } from "../utils/types.js";
-import { uiForms } from "../forms/declaration.js";
-export type AmlEvent = AmlFormEvent | AmlFormEventError | KycCollectionEvent | KycExpirationEvent;
+export type AmlEvent =
+ | AmlFormEvent
+ | AmlFormEventError
+ | KycCollectionEvent
+ | KycExpirationEvent;
+
type AmlFormEvent = {
type: "aml-form";
when: AbsoluteTime;
title: TranslatedString;
justification: Justification;
- metadata: FormMetadata<any>;
- state: AmlExchangeBackend.AmlState;
+ metadata: FormMetadata;
+ state: TalerExchangeApi.AmlState;
threshold: AmountJson;
};
type AmlFormEventError = {
type: "aml-form-error";
when: AbsoluteTime;
title: TranslatedString;
- justification: undefined,
- metadata: undefined,
- state: AmlExchangeBackend.AmlState;
+ justification: undefined;
+ metadata: undefined;
+ state: TalerExchangeApi.AmlState;
threshold: AmountJson;
};
type KycCollectionEvent = {
@@ -58,29 +92,36 @@ function selectSooner(a: WithTime, b: WithTime) {
return AbsoluteTime.cmp(a.when, b.when);
}
-function titleForJustification(op: ReturnType<typeof parseJustification>, i18n: InternationalizationAPI): TranslatedString {
+function titleForJustification(
+ op: ReturnType<typeof parseJustification>,
+ i18n: InternationalizationAPI,
+): TranslatedString {
if (op.type === "ok") {
return op.body.justification.label as TranslatedString;
}
switch (op.case) {
- case "not-json": return "error: the justification is not a form" as TranslatedString
- case "id-not-found": return "error: justification form's id not found" as TranslatedString
- case "version-not-found": return "error: justification form's version not found" as TranslatedString
- case "form-not-found": return `error: justification form not found` as TranslatedString
+ case "not-json":
+ return i18n.str`error: the justification is not a form`;
+ case "id-not-found":
+ return i18n.str`error: justification form's id not found`;
+ case "version-not-found":
+ return i18n.str`error: justification form's version not found`;
+ case "form-not-found":
+ return i18n.str`error: justification form not found`;
default: {
- assertUnreachable(op.case)
+ assertUnreachable(op.case);
}
}
}
export function getEventsFromAmlHistory(
- aml: AmlExchangeBackend.AmlDecisionDetail[],
- kyc: AmlExchangeBackend.KycDetail[],
+ aml: TalerExchangeApi.AmlDecisionDetail[],
+ kyc: TalerExchangeApi.KycDetail[],
i18n: InternationalizationAPI,
+ forms: FormMetadata[],
): AmlEvent[] {
const ae: AmlEvent[] = aml.map((a) => {
-
- const just = parseJustification(a.justification, uiForms.forms(i18n))
+ const just = parseJustification(a.justification, forms);
return {
type: just.type === "ok" ? "aml-form" : "aml-form-error",
state: a.new_state,
@@ -117,104 +158,120 @@ export function getEventsFromAmlHistory(
export function CaseDetails({ account }: { account: string }) {
const [selected, setSelected] = useState<AbsoluteTime>(AbsoluteTime.now());
- const [showForm, setShowForm] = useState<{ justification: Justification, metadata: FormMetadata<any> }>()
+ const [showForm, setShowForm] = useState<{
+ justification: Justification;
+ metadata: FormMetadata;
+ }>();
const { i18n } = useTranslationContext();
- const details = useCaseDetails(account)
+ const details = useCaseDetails(account);
+ const { forms } = useUiFormsContext();
+
+ const allForms = [...forms, ...preloadedForms(i18n)];
if (!details) {
- return <Loading />
+ return <Loading />;
}
if (details instanceof TalerError) {
- return <ErrorLoading error={details} />
+ return <ErrorLoading error={details} />;
}
if (details.type === "fail") {
switch (details.case) {
case HttpStatusCode.Unauthorized:
case HttpStatusCode.Forbidden:
case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict: return <div />
- default: assertUnreachable(details)
+ case HttpStatusCode.Conflict:
+ return <div />;
+ default:
+ assertUnreachable(details);
}
}
- const { aml_history, kyc_attributes } = details.body
+ const { aml_history, kyc_attributes } = details.body;
- const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n);
+ const events = getEventsFromAmlHistory(
+ aml_history,
+ kyc_attributes,
+ i18n,
+ allForms,
+ );
if (showForm !== undefined) {
- return <DefaultForm
- readOnly={true}
- initial={showForm.justification.value}
- form={showForm.metadata.impl(showForm.justification.value)}
- >
- <div class="mt-6 flex items-center justify-end gap-x-6">
- <button
- class="text-sm font-semibold leading-6 text-gray-900"
- onClick={() => {
- setShowForm(undefined)
- }}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- </div>
-
- </DefaultForm>
+ return (
+ <DefaultForm
+ readOnly={true}
+ initial={showForm.justification.value}
+ form={showForm.metadata as any} // FIXME: HERE
+ >
+ <div class="mt-6 flex items-center justify-end gap-x-6">
+ <button
+ class="text-sm font-semibold leading-6 text-gray-900"
+ onClick={() => {
+ setShowForm(undefined);
+ }}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ </div>
+ </DefaultForm>
+ );
}
return (
<div>
<a
- href={Pages.newFormEntry.url({ account })}
+ href={privatePages.caseNew.url({ cid: account })}
class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
>
- <i18n.Translate>
- New AML form
- </i18n.Translate>
+ <i18n.Translate>New AML form</i18n.Translate>
</a>
<header class="flex items-center justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8">
<h1 class="text-base font-semibold leading-7 text-black">
<i18n.Translate>
- Case history for account <span title={account}>{account.substring(0, 16)}...</span>
+ Case history for account{" "}
+ <span title={account}>{account.substring(0, 16)}...</span>
</i18n.Translate>
</h1>
</header>
- <ShowTimeline history={events} onSelect={(e) => {
- switch (e.type) {
- case "aml-form": {
- const { justification, metadata } = e
- setShowForm({ justification, metadata })
- break;
- }
- case "kyc-collection":
- case "kyc-expiration": {
- setSelected(e.when);
- break;
+ <ShowTimeline
+ history={events}
+ onSelect={(e) => {
+ switch (e.type) {
+ case "aml-form": {
+ const { justification, metadata } = e;
+ setShowForm({ justification, metadata });
+ break;
+ }
+ case "kyc-collection":
+ case "kyc-expiration": {
+ setSelected(e.when);
+ break;
+ }
+ case "aml-form-error":
}
- case "aml-form-error":
- }
- }} />
+ }}
+ />
{/* {selected && <ShowEventDetails event={selected} />} */}
{selected && <ShowConsolidated history={events} until={selected} />}
</div>
);
}
-function AmlStateBadge({ state }: { state: AmlExchangeBackend.AmlState }): VNode {
+function AmlStateBadge({ state }: { state: TalerExchangeApi.AmlState }): VNode {
switch (state) {
- case AmlExchangeBackend.AmlState.normal: {
+ case TalerExchangeApi.AmlState.normal: {
return (
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
Normal
</span>
);
}
- case AmlExchangeBackend.AmlState.pending: {
+ case TalerExchangeApi.AmlState.pending: {
return (
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
Pending
</span>
);
}
- case AmlExchangeBackend.AmlState.frozen: {
+ case TalerExchangeApi.AmlState.frozen: {
return (
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
Frozen
@@ -222,93 +279,194 @@ function AmlStateBadge({ state }: { state: AmlExchangeBackend.AmlState }): VNode
);
}
}
- assertUnreachable(state)
+ assertUnreachable(state);
}
-function ShowTimeline({ history, onSelect }: { onSelect: (e: AmlEvent) => void, history: AmlEvent[] }): VNode {
- return <div class="flow-root">
- <ul role="list">
- {history.map((e, idx) => {
- const isLast = history.length - 1 === idx;
- return (
- <li
- data-ok={e.type !== "aml-form-error"}
- class="hover:bg-gray-200 p-2 rounded data-[ok=true]:cursor-pointer"
- onClick={() => {
- onSelect(e);
- }}
- >
- <div class="relative pb-6">
- {!isLast ? (
- <span
- class="absolute left-4 top-4 -ml-px h-full w-1 bg-gray-200"
- aria-hidden="true"
- ></span>
- ) : undefined}
- <div class="relative flex space-x-3">
- {(() => {
- switch (e.type) {
- case "aml-form-error":
- case "aml-form": {
- return <div>
- <AmlStateBadge state={e.state} />
- <span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
- {e.threshold.currency}{" "}
- {Amounts.stringifyValue(e.threshold)}
- </span>
- </div>
- }
- case "kyc-collection": {
- return (
- // <ArrowDownCircleIcon class="h-8 w-8 text-green-700" />
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
- );
- }
- case "kyc-expiration": {
- // return <ClockIcon class="h-8 w-8 text-gray-700" />;
- return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
-
+function ShowTimeline({
+ history,
+ onSelect,
+}: {
+ onSelect: (e: AmlEvent) => void;
+ history: AmlEvent[];
+}): VNode {
+ return (
+ <div class="flow-root">
+ <ul role="list">
+ {history.map((e, idx) => {
+ const isLast = history.length - 1 === idx;
+ return (
+ <li
+ key={idx}
+ data-ok={e.type !== "aml-form-error"}
+ class="hover:bg-gray-200 p-2 rounded data-[ok=true]:cursor-pointer"
+ onClick={() => {
+ onSelect(e);
+ }}
+ >
+ <div class="relative pb-6">
+ {!isLast ? (
+ <span
+ class="absolute left-4 top-4 -ml-px h-full w-1 bg-gray-200"
+ aria-hidden="true"
+ ></span>
+ ) : undefined}
+ <div class="relative flex space-x-3">
+ {(() => {
+ switch (e.type) {
+ case "aml-form-error":
+ case "aml-form": {
+ return (
+ <div>
+ <AmlStateBadge state={e.state} />
+ <span class="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 ">
+ {e.threshold.currency}{" "}
+ {Amounts.stringifyValue(e.threshold)}
+ </span>
+ </div>
+ );
+ }
+ case "kyc-collection": {
+ return (
+ // <ArrowDownCircleIcon class="h-8 w-8 text-green-700" />
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+ />
+ </svg>
+ );
+ }
+ case "kyc-expiration": {
+ // return <ClockIcon class="h-8 w-8 text-gray-700" />;
+ return (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
+ />
+ </svg>
+ );
+ }
}
- }
- assertUnreachable(e)
- })()}
- <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
- {e.type === "aml-form" ?
- <span
- // href={Pages.newFormEntry.url({ account })}
- class="block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
- >
- {e.title}
- </span>
- :
- <p class="text-sm text-gray-900">{e.title}</p>
- }
- <div class="whitespace-nowrap text-right text-sm text-gray-500">
- {e.when.t_ms === "never" ? (
- "never"
+ assertUnreachable(e);
+ })()}
+ <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
+ {e.type === "aml-form" ? (
+ <span
+ // href={Pages.newFormEntry.url({ account })}
+ class="block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
+ >
+ {e.title}
+ </span>
) : (
- <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}>
- {format(e.when.t_ms, "dd MMM yyyy")}
- </time>
+ <p class="text-sm text-gray-900">{e.title}</p>
)}
+ <div class="whitespace-nowrap text-right text-sm text-gray-500">
+ {e.when.t_ms === "never" ? (
+ "never"
+ ) : (
+ <time dateTime={format(e.when.t_ms, "dd MMM yyyy")}>
+ {format(e.when.t_ms, "dd MMM yyyy")}
+ </time>
+ )}
+ </div>
</div>
</div>
</div>
- </div>
- </li>
- );
- })}
- </ul>
- </div>
-
+ </li>
+ );
+ })}
+ </ul>
+ </div>
+ );
}
-function ShowEventDetails({ event }: { event: AmlEvent }): VNode {
- return <div>type {event.type}</div>;
-}
+export type Justification<T = Record<string, unknown>> = {
+ // form values
+ value: T;
+} & Omit<Omit<FormMetadata, "icon">, "config">;
+
+type SimpleFormMetadata = {
+ version?: number;
+ id?: string;
+};
+
+export const codecForSimpleFormMetadata = (): Codec<SimpleFormMetadata> =>
+ buildCodecForObject<SimpleFormMetadata>()
+ .property("id", codecOptional(codecForString()))
+ .property("version", codecOptional(codecForNumber()))
+ .build("SimpleFormMetadata");
+type ParseJustificationFail =
+ | "not-json"
+ | "id-not-found"
+ | "form-not-found"
+ | "version-not-found";
+function parseJustification(
+ s: string,
+ listOfAllKnownForms: FormMetadata[],
+):
+ | OperationOk<{
+ justification: Justification;
+ metadata: FormMetadata;
+ }>
+ | OperationFail<ParseJustificationFail> {
+ try {
+ const justification = JSON.parse(s);
+ const info = codecForSimpleFormMetadata().decode(justification);
+ if (!info.id) {
+ return {
+ type: "fail",
+ case: "id-not-found",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+ if (!info.version) {
+ return {
+ type: "fail",
+ case: "version-not-found",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+ const found = listOfAllKnownForms.find((f) => {
+ return f.id === info.id && f.version === info.version;
+ });
+ if (!found) {
+ return {
+ type: "fail",
+ case: "form-not-found",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+ return {
+ type: "ok",
+ body: {
+ justification,
+ metadata: found,
+ },
+ };
+ } catch (e) {
+ return {
+ type: "fail",
+ case: "not-json",
+ detail: {} as TalerErrorDetail,
+ };
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
new file mode 100644
index 000000000..7801625d0
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -0,0 +1,284 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ HttpStatusCode,
+ TalerExchangeApi,
+ TalerProtocolTimestamp,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Button,
+ FormMetadata,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ RenderAllFieldsByUiConfig,
+ UIHandlerId,
+ convertUiField,
+ getConverterById,
+ useExchangeApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { privatePages } from "../Routing.js";
+import { useUiFormsContext } from "../context/ui-forms.js";
+import { preloadedForms } from "../forms/index.js";
+import {
+ FormErrors,
+ getRequiredFields,
+ getShapeFromFields,
+ useFormState,
+ validateRequiredFields,
+} from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { Justification } from "./CaseDetails.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+
+function searchForm(
+ i18n: InternationalizationAPI,
+ forms: FormMetadata[],
+ formId: string,
+): FormMetadata | undefined {
+ {
+ const found = forms.find((v) => v.id === formId);
+ if (found) return found;
+ }
+ {
+ const pf = preloadedForms(i18n);
+ const found = pf.find((v) => v.id === formId);
+ if (found) return found;
+ }
+ return undefined;
+}
+
+type FormType = {
+ when: AbsoluteTime;
+ state: TalerExchangeApi.AmlState;
+ threshold: AmountJson;
+ comment: string;
+};
+
+export function CaseUpdate({
+ account,
+ type: formId,
+}: {
+ account: string;
+ type: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const officer = useOfficer();
+ const {
+ lib: { exchange: api },
+ } = useExchangeApiContext();
+
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const { config } = useExchangeApiContext();
+ const { forms } = useUiFormsContext();
+ const initial: FormType = {
+ when: AbsoluteTime.now(),
+ state: TalerExchangeApi.AmlState.pending,
+ threshold: Amounts.zeroOfCurrency(config.currency),
+ comment: "",
+ };
+
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
+ const theForm = searchForm(i18n, forms, formId);
+ if (!theForm) {
+ return <div>form with id {formId} not found</div>;
+ }
+
+ const shape: Array<UIHandlerId> = [];
+ const requiredFields: Array<UIHandlerId> = [];
+
+ theForm.config.design.forEach((section) => {
+ Array.prototype.push.apply(shape, getShapeFromFields(section.fields));
+ Array.prototype.push.apply(
+ requiredFields,
+ getRequiredFields(section.fields),
+ );
+ });
+
+ const [form, state] = useFormState<FormType>(shape, initial, (st) => {
+ const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({
+ state: st.state === undefined ? i18n.str`required` : undefined,
+ threshold: !st.threshold ? i18n.str`required` : undefined,
+ when: !st.when ? i18n.str`required` : undefined,
+ });
+
+ const errors = undefinedIfEmpty<FormErrors<FormType> | undefined>(
+ validateRequiredFields(partialErrors, st, requiredFields),
+ );
+
+ if (errors === undefined) {
+ return {
+ status: "ok",
+ result: st as any,
+ errors: undefined,
+ };
+ }
+
+ return {
+ status: "fail",
+ result: st as any,
+ errors,
+ };
+ });
+
+ const validatedForm = state.status !== "ok" ? undefined : state.result;
+
+ const submitHandler =
+ validatedForm === undefined
+ ? undefined
+ : withErrorHandler(
+ () => {
+ const justification: Justification = {
+ id: theForm.id,
+ label: theForm.label,
+ version: theForm.version,
+ value: validatedForm,
+ };
+
+ const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> =
+ {
+ justification: JSON.stringify(justification),
+ decision_time: TalerProtocolTimestamp.now(),
+ h_payto: account,
+ new_state: justification.value
+ .state as TalerExchangeApi.AmlState,
+ new_threshold: Amounts.stringify(
+ justification.value.threshold as AmountJson,
+ ),
+ kyc_requirements: undefined,
+ };
+
+ return api.addDecisionDetails(officer.account, decision);
+ },
+ () => {
+ window.location.href = privatePages.cases.url({});
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.Forbidden:
+ case HttpStatusCode.Unauthorized:
+ return i18n.str`Wrong credentials for "${officer.account}"`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`Officer or account not found`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`Officer disabled or more recent decision was already submitted.`;
+ default:
+ assertUnreachable(fail);
+ }
+ },
+ );
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
+ {theForm.config.design.map((section, i) => {
+ if (!section) return <Fragment />;
+ return (
+ <div
+ key={i}
+ class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
+ >
+ <div class="px-4 sm:px-0">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ {section.title}
+ </h2>
+ {section.description && (
+ <p class="mt-1 text-sm leading-6 text-gray-600">
+ {section.description}
+ </p>
+ )}
+ </div>
+ <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md md:col-span-2">
+ <div class="p-3">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <RenderAllFieldsByUiConfig
+ key={i}
+ fields={convertUiField(
+ i18n,
+ section.fields,
+ form,
+ getConverterById,
+ )}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+
+ <div class="mt-6 flex items-center justify-end gap-x-6">
+ <a
+ href={privatePages.caseDetails.url({ cid: account })}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <Button
+ type="submit"
+ handler={submitHandler}
+ disabled={!submitHandler}
+ class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </Button>
+ </div>
+ </Fragment>
+ );
+}
+
+export function SelectForm({ account }: { account: string }) {
+ const { i18n } = useTranslationContext();
+ const { forms } = useUiFormsContext();
+ const pf = preloadedForms(i18n);
+ return (
+ <div>
+ <pre>New form for account: {account.substring(0, 16)}...</pre>
+ {forms.map((form) => {
+ return (
+ <a
+ key={form.id}
+ href={privatePages.caseUpdate.url({ cid: account, type: form.id })}
+ class="m-4 block rounded-md w-fit border-0 p-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-600"
+ >
+ {form.label}
+ </a>
+ );
+ })}
+ {pf.map((form) => {
+ return (
+ <a
+ key={form.id}
+ href={privatePages.caseUpdate.url({ cid: account, type: form.id })}
+ class="m-4 block rounded-md w-fit border-0 p-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-600"
+ >
+ {form.label}
+ </a>
+ );
+ })}
+ </div>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
index 3b9c8dacf..22a6d1867 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -20,23 +20,22 @@
*/
import * as tests from "@gnu-taler/web-util/testing";
-import {
- CasesUI as TestedComponent,
-} from "./Cases.js";
-import { AmountString } from "@gnu-taler/taler-util";
-import { AmlExchangeBackend } from "../utils/types.js";
+import { CasesUI as TestedComponent } from "./Cases.js";
+import { AmountString, TalerExchangeApi } from "@gnu-taler/taler-util";
export default {
title: "cases",
};
export const OneRow = tests.createExample(TestedComponent, {
- filter: AmlExchangeBackend.AmlState.normal,
+ filter: TalerExchangeApi.AmlState.normal,
onChangeFilter: () => null,
- records: [{
- current_state: AmlExchangeBackend.AmlState.normal,
- h_payto: "QWEQWEQWEQWE",
- rowid: 1,
- threshold: "USD:1" as AmountString
- }]
+ records: [
+ {
+ current_state: TalerExchangeApi.AmlState.normal,
+ h_payto: "QWEQWEQWEQWE",
+ rowid: 1,
+ threshold: "USD:1" as AmountString,
+ },
+ ],
});
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index 88580a4ce..f66eca33f 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -1,203 +1,331 @@
-import { HttpStatusCode, TalerError, TalerExchangeApi, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util";
-import { ErrorLoading, Loading, createNewForm, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
-import { useState } from "preact/hooks";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpStatusCode,
+ TalerError,
+ TalerExchangeApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ ErrorLoading,
+ InputChoiceHorizontal,
+ Loading,
+ UIHandlerId,
+ amlStateConverter,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
import { useCases } from "../hooks/useCases.js";
-import { Pages } from "../pages.js";
+import { privatePages } from "../Routing.js";
+import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
import { Officer } from "./Officer.js";
-import { amlStateConverter } from "../utils/converter.js";
-import { AmlExchangeBackend } from "../utils/types.js";
-export function CasesUI({ records, filter, onChangeFilter, onFirstPage, onNext }: { onFirstPage?: () => void, onNext?: () => void, filter: AmlExchangeBackend.AmlState, onChangeFilter: (f: AmlExchangeBackend.AmlState) => void, records: TalerExchangeApi.AmlRecord[] }): VNode {
+type FormType = {
+ state: TalerExchangeApi.AmlState;
+};
+
+export function CasesUI({
+ records,
+ filter,
+ onChangeFilter,
+ onFirstPage,
+ onNext,
+}: {
+ onFirstPage?: () => void;
+ onNext?: () => void;
+ filter: TalerExchangeApi.AmlState;
+ onChangeFilter: (f: TalerExchangeApi.AmlState) => void;
+ records: TalerExchangeApi.AmlRecord[];
+}): VNode {
const { i18n } = useTranslationContext();
- const form = createNewForm<{ state: AmlExchangeBackend.AmlState }>();
-
- return <div>
- <div class="sm:flex sm:items-center">
- <div class="px-2 sm:flex-auto">
- <h1 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>
- Cases
- </i18n.Translate>
- </h1>
- <p class="mt-2 text-sm text-gray-700 w-80">
- <i18n.Translate>
- A list of all the account with the status
- </i18n.Translate>
- </p>
- </div>
- <div class="px-2">
- <form.Provider
- initial={{ state: filter }}
- onUpdate={(v) => {
- onChangeFilter(v.state ?? filter);
- }}
- onSubmit={(v) => { }}
- >
- <form.InputChoiceHorizontal
+ const [form, status] = useFormState<FormType>(
+ [".state"] as Array<UIHandlerId>,
+ {
+ state: filter,
+ },
+ (state) => {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ state: state.state === undefined ? i18n.str`required` : undefined,
+ });
+ if (errors === undefined) {
+ const result: FormType = {
+ state: state.state!,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<FormType> = {
+ state: state.state,
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ },
+ );
+ useEffect(() => {
+ if (status.status === "ok" && filter !== status.result.state) {
+ onChangeFilter(status.result.state);
+ }
+ }, [form?.state?.value]);
+
+ return (
+ <div>
+ <div class="sm:flex sm:items-center">
+ <div class="px-2 sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Cases</i18n.Translate>
+ </h1>
+ <p class="mt-2 text-sm text-gray-700 w-80">
+ <i18n.Translate>
+ A list of all the account with the status
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="px-2">
+ <InputChoiceHorizontal<FormType, "state">
name="state"
label={i18n.str`Filter`}
+ handler={form.state}
converter={amlStateConverter}
choices={[
{
label: i18n.str`Pending`,
- value: AmlExchangeBackend.AmlState.pending,
+ value: "pending",
},
{
label: i18n.str`Frozen`,
- value: AmlExchangeBackend.AmlState.frozen,
+ value: "frozen",
},
{
label: i18n.str`Normal`,
- value: AmlExchangeBackend.AmlState.normal,
+ value: "normal",
},
]}
/>
-
- </form.Provider>
+ </div>
</div>
- </div>
- <div class="mt-8 flow-root">
- <div class="overflow-x-auto">
- {!records.length ? (
- <div>empty result </div>
- ) : (
- <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
- <table class="min-w-full divide-y divide-gray-300">
- <thead>
- <tr>
- <th
- scope="col"
- class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80"
- >
- <i18n.Translate>
- Account Id
- </i18n.Translate>
- </th>
- <th
- scope="col"
- class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
- >
- <i18n.Translate>
- Status
- </i18n.Translate>
- </th>
- <th
- scope="col"
- class="sm:hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
- >
- <i18n.Translate>
- Threshold
- </i18n.Translate>
- </th>
- </tr>
- </thead>
- <tbody class="divide-y divide-gray-200 bg-white">
- {records.map((r) => {
- return (
- <tr class="hover:bg-gray-100 ">
- <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
- <div class="text-gray-900">
- <a
- href={Pages.account.url({ account: r.h_payto })}
- class="text-indigo-600 hover:text-indigo-900"
- >
- {r.h_payto.substring(0, 16)}...
- </a>
- </div>
- </td>
- <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
- {((state: AmlExchangeBackend.AmlState): VNode => {
- switch (state) {
- case AmlExchangeBackend.AmlState.normal: {
- return (
- <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
- Normal
- </span>
- );
- }
- case AmlExchangeBackend.AmlState.pending: {
- return (
- <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
- Pending
- </span>
- );
- }
- case AmlExchangeBackend.AmlState.frozen: {
- return (
- <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
- Frozen
- </span>
- );
+ <div class="mt-8 flow-root">
+ <div class="overflow-x-auto">
+ {!records.length ? (
+ <div>empty result </div>
+ ) : (
+ <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80"
+ >
+ <i18n.Translate>Account Id</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
+ >
+ <i18n.Translate>Status</i18n.Translate>
+ </th>
+ <th
+ scope="col"
+ class="sm:hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
+ >
+ <i18n.Translate>Threshold</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-gray-200 bg-white">
+ {records.map((r) => {
+ return (
+ <tr key={r.h_payto} class="hover:bg-gray-100 ">
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
+ <div class="text-gray-900">
+ <a
+ href={privatePages.caseDetails.url({
+ cid: r.h_payto,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ {r.h_payto.substring(0, 16)}...
+ </a>
+ </div>
+ </td>
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
+ {((state: TalerExchangeApi.AmlState): VNode => {
+ switch (state) {
+ case TalerExchangeApi.AmlState.normal: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
+ Normal
+ </span>
+ );
+ }
+ case TalerExchangeApi.AmlState.pending: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20">
+ Pending
+ </span>
+ );
+ }
+ case TalerExchangeApi.AmlState.frozen: {
+ return (
+ <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20">
+ Frozen
+ </span>
+ );
+ }
}
- }
- })(r.current_state)}
- </td>
- <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
- {r.threshold}
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- <Pagination onFirstPage={onFirstPage} onNext={onNext} />
- </div>
- )}
+ })(r.current_state)}
+ </td>
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
+ {r.threshold}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ <Pagination onFirstPage={onFirstPage} onNext={onNext} />
+ </div>
+ )}
+ </div>
</div>
</div>
- </div>
-
+ );
}
-
export function Cases() {
- const [stateFilter, setStateFilter] = useState(AmlExchangeBackend.AmlState.pending);
+ const [stateFilter, setStateFilter] = useState(
+ TalerExchangeApi.AmlState.pending,
+ );
const list = useCases(stateFilter);
+ const { i18n } = useTranslationContext();
if (!list) {
- return <Loading />
+ return <Loading />;
}
if (list instanceof TalerError) {
- return <ErrorLoading error={list} />
+ return <ErrorLoading error={list} />;
}
- if (list.data.type === "fail") {
- switch (list.data.case) {
- case HttpStatusCode.Unauthorized:
- case HttpStatusCode.Forbidden:
+ if (list.type === "fail") {
+ switch (list.case) {
+ case HttpStatusCode.Forbidden: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account doesn't have access. Request account activation
+ sending your public key.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
+ case HttpStatusCode.Unauthorized: {
+ return (
+ <Fragment>
+ <Attention type="danger" title={i18n.str`Operation denied`}>
+ <i18n.Translate>
+ This account is not allowed to perform list the cases.
+ </i18n.Translate>
+ </Attention>
+ <Officer />
+ </Fragment>
+ );
+ }
case HttpStatusCode.NotFound:
- case HttpStatusCode.Conflict: return <Officer />
- default: assertUnreachable(list.data)
+ case HttpStatusCode.Conflict:
+ return <Officer />;
+ default:
+ assertUnreachable(list);
}
}
- const { records } = list.data.body
-
- return <CasesUI
- records={records}
- onFirstPage={list.pagination && !list.pagination.isFirstPage ? list.pagination.reset : undefined}
- onNext={list.pagination && !list.pagination.isLastPage ? list.pagination.loadMore : undefined}
- filter={stateFilter}
- onChangeFilter={setStateFilter}
- />
+ return (
+ <CasesUI
+ records={list.body}
+ onFirstPage={list.isFirstPage ? undefined : list.loadFirst}
+ onNext={list.isLastPage ? undefined : list.loadNext}
+ filter={stateFilter}
+ onChangeFilter={(d) => {
+ setStateFilter(d);
+ }}
+ />
+ );
}
-export const PeopleIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
-</svg>
+export const PeopleIcon = () => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
+ />
+ </svg>
+);
-export const HomeIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
-</svg>
+export const HomeIcon = () => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
+ />
+ </svg>
+);
-function Pagination({ onFirstPage, onNext }: { onFirstPage?: () => void, onNext?: () => void, }) {
- const { i18n } = useTranslationContext()
+function Pagination({
+ onFirstPage,
+ onNext,
+}: {
+ onFirstPage?: () => void;
+ onNext?: () => void;
+}) {
+ const { i18n } = useTranslationContext();
return (
- <nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination">
+ <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"
@@ -215,6 +343,5 @@ function Pagination({ onFirstPage, onNext }: { onFirstPage?: () => void, onNext?
</button>
</div>
</nav>
-
- )
+ );
}
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 603813f8e..87310aa27 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -1,26 +1,130 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
import {
- createNewForm,
- notifyError,
+ Button,
+ InputLine,
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ UIHandlerId,
+ useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
-import { useSettings } from "../hooks/useSettings.js";
+import {
+ FormErrors,
+ FormStatus,
+ FormValues,
+ RecursivePartial,
+ useFormState,
+} from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { usePreferences } from "../hooks/preferences.js";
+
+type FormType = {
+ password: string;
+ repeat: string;
+};
+function createFormValidator(
+ i18n: InternationalizationAPI,
+ allowInsecurePassword: boolean,
+) {
+ return function check(
+ state: RecursivePartial<FormValues<FormType>>,
+ ): FormStatus<FormType> {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ password: !state.password
+ ? i18n.str`required`
+ : allowInsecurePassword
+ ? undefined
+ : state.password.length < 8
+ ? i18n.str`should have at least 8 characters`
+ : !state.password.match(/[a-z]/) && state.password.match(/[A-Z]/)
+ ? i18n.str`should have lowercase and uppercase characters`
+ : !state.password.match(/\d/)
+ ? i18n.str`should have numbers`
+ : !state.password.match(/[^a-zA-Z\d]/)
+ ? i18n.str`should have at least one character which is not a number or letter`
+ : undefined,
+
+ repeat: !state.repeat
+ ? i18n.str`required`
+ : state.password !== state.repeat
+ ? i18n.str`doesn't match`
+ : undefined,
+ });
+
+ if (errors === undefined) {
+ const result: FormType = {
+ password: state.password!,
+ repeat: state.repeat!,
+ };
+ return {
+ status: "ok",
+ result,
+ errors,
+ };
+ }
+ const result: RecursivePartial<FormType> = {
+ password: state.password,
+ repeat: state.repeat,
+ };
+ return {
+ status: "fail",
+ result,
+ errors,
+ };
+ };
+}
+
+export function undefinedIfEmpty<T extends object | undefined>(obj: T): T | undefined {
+ if (obj === undefined) return undefined;
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
-export function CreateAccount({
- onNewAccount,
-}: {
- onNewAccount: (password: string) => void;
-}): VNode {
+export function CreateAccount(): VNode {
const { i18n } = useTranslationContext();
- const Form = createNewForm<{
- password: string;
- repeat: string;
- }>();
- const [settings] = useSettings()
+ const [settings] = usePreferences();
+ const officer = useOfficer();
+
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const [form, status] = useFormState<FormType>(
+ [".password", ".repeat"] as Array<UIHandlerId>,
+ {
+ password: undefined,
+ repeat: undefined,
+ },
+ createFormValidator(i18n, settings.allowInsecurePassword),
+ );
+
+ const createAccountHandler =
+ status.status === "fail" || officer.state !== "not-found"
+ ? undefined
+ : withErrorHandler(
+ async () => officer.create(form.password!.value!),
+ () => {},
+ );
return (
<div class="flex min-h-full flex-col ">
+ <LocalNotificationBanner notification={notification} />
+
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<i18n.Translate>Create account</i18n.Translate>
@@ -29,78 +133,65 @@ export function CreateAccount({
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
- <Form.Provider
- computeFormState={(v) => {
- return {
- password: {
- error: !v.password
- ? i18n.str`required`
- : settings.allowInsecurePassword
- ? undefined
- : v.password.length < 8
- ? i18n.str`should have at least 8 characters`
- : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/)
- ? i18n.str`should have lowercase and uppercase characters`
- : !v.password.match(/\d/)
- ? i18n.str`should have numbers`
- : !v.password.match(/[^a-zA-Z\d]/)
- ? i18n.str`should have at least one character which is not a number or letter`
- : undefined,
- },
- repeat: {
- error: !v.repeat
- ? i18n.str`required`
- : v.repeat !== v.password
- ? i18n.str`doesn't match`
- : undefined,
- },
- };
- }}
- onSubmit={async (v, s) => {
- const error = s?.password?.error ?? s?.repeat?.error;
- if (error) {
- notifyError(
- i18n.str`Can't create account`,
- error as TranslatedString,
- );
- } else {
- onNewAccount(v.password!);
- }
+ <form
+ class="space-y-6"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
}}
+ autoCapitalize="none"
+ autoCorrect="off"
>
- <div class="mb-4">
- <Form.InputLine
+ <div class="mt-2">
+ <InputLine<FormType, "password">
label={i18n.str`Password`}
name="password"
type="password"
- help={
- settings.allowInsecurePassword
- ? i18n.str`short password are insecure, turn off insecure password in settings`
- : i18n.str`lower and upper case letters, number and special character`
- }
required
+ handler={form.password}
/>
</div>
- <div class="mb-4">
- <Form.InputLine
+
+ <div class="mt-2">
+ <InputLine<FormType, "repeat">
label={i18n.str`Repeat password`}
name="repeat"
type="password"
required
+ handler={form.repeat}
/>
</div>
<div class="mt-8">
- <button
+ <Button
type="submit"
- class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ disabled={!createAccountHandler}
+ class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={createAccountHandler}
>
<i18n.Translate>Create</i18n.Translate>
- </button>
+ </Button>
</div>
- </Form.Provider>
+ </form>
</div>
</div>
</div>
);
}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null) {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
+ }, 100);
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
index ff800ebdc..3d6e14f22 100644
--- a/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
+++ b/packages/aml-backoffice-ui/src/pages/HandleAccountNotReady.tsx
@@ -1,8 +1,23 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { assertUnreachable } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
-import { OfficerNotReady } from "../hooks/useOfficer.js";
+import { OfficerNotReady } from "../hooks/officer.js";
import { CreateAccount } from "./CreateAccount.js";
import { UnlockAccount } from "./UnlockAccount.js";
-import { assertUnreachable } from "@gnu-taler/taler-util";
export function HandleAccountNotReady({
officer,
@@ -10,26 +25,11 @@ export function HandleAccountNotReady({
officer: OfficerNotReady;
}): VNode {
if (officer.state === "not-found") {
- return (
- <CreateAccount
- onNewAccount={(password) => {
- officer.create(password);
- }}
- />
- );
+ return <CreateAccount />;
}
if (officer.state === "locked") {
- return (
- <UnlockAccount
- onRemoveAccount={() => {
- officer.forget();
- }}
- onAccountUnlocked={async (pwd) => {
- await officer.tryUnlock(pwd);
- }}
- />
- );
+ return <UnlockAccount />;
}
- assertUnreachable(officer)
+ assertUnreachable(officer);
}
diff --git a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx b/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
deleted file mode 100644
index df97cc3a4..000000000
--- a/packages/aml-backoffice-ui/src/pages/NewFormEntry.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import { AbsoluteTime, Amounts, HttpStatusCode, TalerExchangeApi, TalerProtocolTimestamp, TranslatedString } from "@gnu-taler/taler-util";
-import { LocalNotificationBanner, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { useExchangeApiContext } from "../context/config.js";
-import { useOfficer } from "../hooks/useOfficer.js";
-import { Pages } from "../pages.js";
-import { AntiMoneyLaunderingForm } from "./AntiMoneyLaunderingForm.js";
-import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
-import { uiForms } from "../forms/declaration.js";
-
-export function NewFormEntry({
- account,
- type,
-}: {
- account?: string;
- type?: string;
-}): VNode {
- const { i18n } = useTranslationContext()
- const officer = useOfficer();
- const { api } = useExchangeApiContext()
- const [notification, notify, handleError] = useLocalNotification()
-
- if (!account) {
- return <div>no account</div>;
- }
- if (!type) {
- return <SelectForm account={account} />;
- }
- if (officer.state !== "ready") {
- return <HandleAccountNotReady officer={officer} />;
- }
-
- return (
- <Fragment>
- <LocalNotificationBanner notification={notification} />
-
- <AntiMoneyLaunderingForm
- account={account}
- formId={type}
- onSubmit={async (justification, new_state, new_threshold) => {
-
- const decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig"> = {
- justification: JSON.stringify(justification),
- decision_time: TalerProtocolTimestamp.now(),
- h_payto: account,
- new_state,
- new_threshold: Amounts.stringify(new_threshold),
- kyc_requirements: undefined
- }
- await handleError(async () => {
- const resp = await api.addDecisionDetails(officer.account, decision);
- if (resp.type === "ok") {
- window.location.href = Pages.cases.url;
- return;
- }
- switch (resp.case) {
- case HttpStatusCode.Forbidden:
- case HttpStatusCode.Unauthorized: return notify({
- type: "error",
- title: i18n.str`Wrong credentials for "${officer.account}"`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- when: AbsoluteTime.now(),
- })
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`Officer or account not found`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- when: AbsoluteTime.now(),
- })
- case HttpStatusCode.Conflict: return notify({
- type: "error",
- title: i18n.str`Officer disabled or more recent decision was already submitted.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- when: AbsoluteTime.now(),
- })
- }
- })
- }}
- />
- </Fragment>
- );
-}
-
-function SelectForm({ account }: { account: string }) {
- const { i18n } = useTranslationContext()
- return (
- <div>
- <pre>New form for account: {account.substring(0, 16)}...</pre>
- {uiForms.forms(i18n).map((form, idx) => {
- return (
- <a
- href={Pages.newFormEntry.url({ account, type: form.id })}
- class="m-4 block rounded-md w-fit border-0 p-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-600"
- >
- {form.label}
- </a>
- );
- })}
- </div>
- );
-}
diff --git a/packages/aml-backoffice-ui/src/pages/Officer.tsx b/packages/aml-backoffice-ui/src/pages/Officer.tsx
index ec8327814..39359cd5e 100644
--- a/packages/aml-backoffice-ui/src/pages/Officer.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Officer.tsx
@@ -1,19 +1,39 @@
-import { Fragment, h } from "preact";
-import { useOfficer } from "../hooks/useOfficer.js";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ useExchangeApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import { useOfficer } from "../hooks/officer.js";
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { uiSettings } from "../settings.js";
-import { getInitialBackendBaseURL } from "../hooks/useBackend.js";
+import { useUiSettingsContext } from "../context/ui-settings.js";
export function Officer() {
const officer = useOfficer();
- const { i18n } = useTranslationContext()
+ const settings = useUiSettingsContext();
+ const { lib } = useExchangeApiContext();
+
+ const { i18n } = useTranslationContext();
if (officer.state !== "ready") {
return <HandleAccountNotReady officer={officer} />;
}
- const url = new URL(getInitialBackendBaseURL())
- const signupEmail = uiSettings.signupEmail ?? `aml-signup@${url.hostname}`
+ const url = new URL("./", lib.exchange.baseUrl);
+ const signupEmail = settings.signupEmail ?? `aml-signup@${url.hostname}`;
return (
<div>
@@ -25,7 +45,11 @@ export function Officer() {
</div>
<p>
<a
- href={`mailto:${signupEmail}?subject=${encodeURIComponent("Request AML signup")}&body=${encodeURIComponent(`I want my AML account\n\n\nPubKey: ${officer.account.id}`)}`}
+ href={`mailto:${signupEmail}?subject=${encodeURIComponent(
+ "Request AML signup",
+ )}&body=${encodeURIComponent(
+ `I want my AML account\n\n\nPubKey: ${officer.account.id}`,
+ )}`}
target="_blank"
rel="noreferrer"
class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
index f985e6ff5..714bf6580 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,99 +19,114 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { addDays } from "date-fns";
import {
- ShowConsolidated as TestedComponent,
-} from "./ShowConsolidated.js";
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { InternationalizationAPI } from "@gnu-taler/web-util/browser";
import * as tests from "@gnu-taler/web-util/testing";
import { getEventsFromAmlHistory } from "./CaseDetails.js";
-import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
-import { InternationalizationAPI } from "@gnu-taler/web-util/browser";
+import { ShowConsolidated as TestedComponent } from "./ShowConsolidated.js";
export default {
title: "show consolidated",
};
const nullTranslator: InternationalizationAPI = {
- str: (str: any) => str,
- singular: (str: any) => str,
- translate: (str: any) => str,
- Translate: (str: any) => str,
-}
+ str: (str: TemplateStringsArray) => str.join() as TranslatedString,
+ singular: (str: TemplateStringsArray) => str.join() as TranslatedString,
+ translate: (str: TemplateStringsArray) => [str.join()] as TranslatedString[],
+ Translate: () => undefined as unknown,
+};
export const WithEmptyHistory = tests.createExample(TestedComponent, {
- history: getEventsFromAmlHistory([], [], nullTranslator),
- until: AbsoluteTime.now()
+ history: getEventsFromAmlHistory([], [], nullTranslator, []),
+ until: AbsoluteTime.now(),
});
export const WithSomeEvents = tests.createExample(TestedComponent, {
- history: getEventsFromAmlHistory([
- {
- "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700208199
- }
- },
- {
- "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700208211
- }
- },
- {
- "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- "justification": "{\"index\":0,\"name\":\"Simple comment\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700207199558},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"test\"}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700208220
- }
- },
- {
- "decider_pub": "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
- "justification": "{\"index\":4,\"name\":\"Declaration for trusts (902.13e)\",\"value\":{\"fullName\":\"loggedIn_user_fullname\",\"when\":{\"t_ms\":1700208362854},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"contractingPartner\":\"f\",\"knownAs\":\"a\",\"trust\":{\"name\":\"b\",\"type\":\"discretionary\",\"revocability\":\"irrevocable\"}}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700208385
- }
- },
- {
- "decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
- "justification": "{\"id\":\"simple_comment\",\"label\":\"Simple comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488420810},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"qwe\"}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700488423
- }
- },
- {
- "decider_pub": "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
- "justification": "{\"id\":\"simple_comment\",\"label\":\"Simple comment\",\"version\":1,\"value\":{\"when\":{\"t_ms\":1700488671251},\"state\":1,\"threshold\":{\"currency\":\"STATER\",\"fraction\":0,\"value\":0},\"comment\":\"asd asd asd \"}}",
- "new_threshold": "STATER:0",
- "new_state": 1,
- "decision_time": {
- "t_s": 1700488677
- }
- }
- ], [{
- collection_time: AbsoluteTime.toProtocolTimestamp(
- AbsoluteTime.subtractDuraction(AbsoluteTime.now(), Duration.fromPrettyString("1d"))
- ),
- expiration_time: { t_s: "never" },
- provider_section: "asd",
- attributes: {
- email: "sebasjm@qwdde.com"
- }
- }], nullTranslator),
- until: AbsoluteTime.now()
+ history: getEventsFromAmlHistory(
+ [
+ {
+ decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ justification:
+ '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700208199,
+ },
+ },
+ {
+ decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ justification:
+ '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700208211,
+ },
+ },
+ {
+ decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ justification:
+ '{"index":0,"name":"Simple comment","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700207199558},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"test"}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700208220,
+ },
+ },
+ {
+ decider_pub: "JD70N2XZ8FZKB7C146ZWR6XBDCS4Z84PJKJMPB73PMJ2B1X35ZFG",
+ justification:
+ '{"index":4,"name":"Declaration for trusts (902.13e)","value":{"fullName":"loggedIn_user_fullname","when":{"t_ms":1700208362854},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"contractingPartner":"f","knownAs":"a","trust":{"name":"b","type":"discretionary","revocability":"irrevocable"}}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700208385,
+ },
+ },
+ {
+ decider_pub: "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
+ justification:
+ '{"id":"simple_comment","label":"Simple comment","version":1,"value":{"when":{"t_ms":1700488420810},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"qwe"}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700488423,
+ },
+ },
+ {
+ decider_pub: "6CD3J8XSKWQPFFDJY4SP4RK2D7T7WW7JRJDTXHNZY7YKGXDCE2QG",
+ justification:
+ '{"id":"simple_comment","label":"Simple comment","version":1,"value":{"when":{"t_ms":1700488671251},"state":1,"threshold":{"currency":"STATER","fraction":0,"value":0},"comment":"asd asd asd "}}',
+ new_threshold: "STATER:0" as AmountString,
+ new_state: 1,
+ decision_time: {
+ t_s: 1700488677,
+ },
+ },
+ ],
+ [
+ {
+ collection_time: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.subtractDuraction(
+ AbsoluteTime.now(),
+ Duration.fromPrettyString("1d"),
+ ),
+ ),
+ expiration_time: { t_s: "never" },
+ provider_section: "asd",
+ attributes: {
+ email: "sebasjm@qwdde.com",
+ },
+ },
+ ],
+ nullTranslator,
+ [],
+ ),
+ until: AbsoluteTime.now(),
});
-
-
-
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index ad350c0e6..cdc5d0bc1 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -1,10 +1,34 @@
-import { AbsoluteTime, AmountJson, TranslatedString } from "@gnu-taler/taler-util";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ AmountJson,
+ TalerExchangeApi,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ DefaultForm,
+ FormConfiguration,
+ UIFormElementConfig,
+ UIHandlerId,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { AmlEvent } from "./CaseDetails.js";
-import { DefaultForm, FlexibleForm, UIFormField, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { amlStateConverter } from "../utils/converter.js";
-import { AmlExchangeBackend } from "../utils/types.js";
export function ShowConsolidated({
history,
@@ -17,91 +41,76 @@ export function ShowConsolidated({
const cons = getConsolidated(history, until);
- const form: FlexibleForm<Consolidated> = {
- behavior: (form) => {
- return {
- aml: {
- threshold: {
- hidden: !form.aml
- },
- since: {
- hidden: !form.aml
- },
- state: {
- hidden: !form.aml
- }
- }
- };
- },
+ const form: FormConfiguration = {
+ type: "double-column",
design: [
{
title: i18n.str`AML`,
fields: [
{
type: "amount",
- props: {
- label: i18n.str`Threshold`,
- name: "aml.threshold",
- },
+ id: ".aml.threshold" as UIHandlerId,
+ currency: "NETZBON",
+ label: i18n.str`Threshold`,
+ name: "aml.threshold",
},
{
type: "choiceHorizontal",
- props: {
- label: i18n.str`State`,
- name: "aml.state",
- converter: amlStateConverter,
- choices: [
- {
- label: i18n.str`Frozen`,
- value: AmlExchangeBackend.AmlState.frozen,
- },
- {
- label: i18n.str`Pending`,
- value: AmlExchangeBackend.AmlState.pending,
- },
- {
- label: i18n.str`Normal`,
- value: AmlExchangeBackend.AmlState.normal,
- },
- ],
- },
+ label: i18n.str`State`,
+ name: "aml.state",
+ id: ".aml.state" as UIHandlerId,
+ choices: [
+ {
+ label: i18n.str`Frozen`,
+ value: "frozen",
+ },
+ {
+ label: i18n.str`Pending`,
+ value: "pending",
+ },
+ {
+ label: i18n.str`Normal`,
+ value: "normal",
+ },
+ ],
},
],
},
Object.entries(cons.kyc).length > 0
? {
- title: i18n.str`KYC`,
- fields: Object.entries(cons.kyc).map(([key, field]) => {
- const result: UIFormField = {
- type: "text",
- props: {
+ title: i18n.str`KYC`,
+ fields: Object.entries(cons.kyc).map(([key, field]) => {
+ const result: UIFormElementConfig = {
+ type: "text",
label: key as TranslatedString,
+ id: `kyc.${key}.value` as UIHandlerId,
name: `kyc.${key}.value`,
- help: `${field.provider} since ${field.since.t_ms === "never"
- ? "never"
- : format(field.since.t_ms, "dd/MM/yyyy")
- }` as TranslatedString,
- },
- };
- return result;
- }),
- }
- : undefined,
+ help: `${field.provider} since ${
+ field.since.t_ms === "never"
+ ? "never"
+ : format(field.since.t_ms, "dd/MM/yyyy")
+ }` as TranslatedString,
+ };
+ return result;
+ }),
+ }
+ : undefined!,
],
};
return (
<Fragment>
<h1 class="text-base font-semibold leading-7 text-black">
- Consolidated information {until.t_ms === "never"
+ Consolidated information{" "}
+ {until.t_ms === "never"
? ""
: `after ${format(until.t_ms, "dd MMMM yyyy")}`}
</h1>
<DefaultForm
key={`${String(Date.now())}`}
- form={form}
+ form={form as any}
initial={cons}
readOnly
- onUpdate={() => { }}
+ onUpdate={() => {}}
/>
</Fragment>
);
@@ -109,13 +118,13 @@ export function ShowConsolidated({
interface Consolidated {
aml: {
- state: AmlExchangeBackend.AmlState;
+ state: TalerExchangeApi.AmlState;
threshold: AmountJson;
since: AbsoluteTime;
};
kyc: {
[field: string]: {
- value: any;
+ value: unknown;
provider: string;
since: AbsoluteTime;
};
@@ -128,13 +137,13 @@ function getConsolidated(
): Consolidated {
const initial: Consolidated = {
aml: {
- state: AmlExchangeBackend.AmlState.normal,
+ state: TalerExchangeApi.AmlState.normal,
threshold: {
currency: "ARS",
value: 1000,
fraction: 0,
},
- since: AbsoluteTime.never()
+ since: AbsoluteTime.never(),
},
kyc: {},
};
@@ -153,14 +162,15 @@ function getConsolidated(
prev.aml = {
since: cur.when,
state: cur.state,
- threshold: cur.threshold
- }
+ threshold: cur.threshold,
+ };
break;
}
case "kyc-collection": {
Object.keys(cur.values).forEach((field) => {
+ const value = (cur.values as Record<string, unknown>)[field];
prev.kyc[field] = {
- value: (cur.values as any)[field],
+ value,
provider: cur.provider,
since: cur.when,
};
@@ -170,4 +180,4 @@ function getConsolidated(
}
return prev;
}, initial);
-} \ No newline at end of file
+}
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index 1b0342b12..084e639bf 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -1,78 +1,130 @@
-import { TranslatedString, UnwrapKeyError } from "@gnu-taler/taler-util";
-import { createNewForm, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ Button,
+ InputLine,
+ LocalNotificationBanner,
+ UIHandlerId,
+ useLocalNotificationHandler,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
+import { FormErrors, useFormState } from "../hooks/form.js";
+import { useOfficer } from "../hooks/officer.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
+
+type FormType = {
+ password: string;
+};
+
+export function UnlockAccount(): VNode {
+ const { i18n } = useTranslationContext();
+
+ const officer = useOfficer();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
-export function UnlockAccount({
- onAccountUnlocked,
- onRemoveAccount,
-}: {
- onAccountUnlocked: (password: string) => Promise<void>;
- onRemoveAccount: () => void;
-}): VNode {
- const { i18n } = useTranslationContext()
- const Form = createNewForm<{
- password: string;
- }>();
+ const [form, status] = useFormState<FormType>(
+ [".password"] as Array<UIHandlerId>,
+ {
+ password: undefined,
+ },
+ (state) => {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ password: !state.password ? i18n.str`required` : undefined,
+ });
+ if (errors === undefined) {
+ return {
+ status: "ok",
+ result: state as FormType,
+ errors,
+ };
+ }
+ return {
+ status: "fail",
+ result: state,
+ errors,
+ };
+ },
+ );
+
+ const unlockHandler =
+ status.status === "fail" || officer.state !== "locked"
+ ? undefined
+ : withErrorHandler(
+ async () => officer.tryUnlock(form.password!.value!),
+ () => {},
+ );
+
+ const forgetHandler =
+ status.status === "fail" || officer.state !== "locked"
+ ? undefined
+ : withErrorHandler(
+ async () => officer.forget(),
+ () => {},
+ );
return (
<div class="flex min-h-full flex-col ">
+ <LocalNotificationBanner notification={notification} />
+
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h1 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<i18n.Translate>Account locked</i18n.Translate>
</h1>
<p class="mt-6 text-lg leading-8 text-gray-600">
- <i18n.Translate>Your account is normally locked anytime you reload. To unlock type
- your password again.</i18n.Translate>
+ <i18n.Translate>
+ Your account is normally locked anytime you reload. To unlock type
+ your password again.
+ </i18n.Translate>
</p>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
<div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
- <Form.Provider
- onSubmit={async (v) => {
- try {
- await onAccountUnlocked(v.password!);
- notifyInfo(i18n.str`Account unlocked`);
- } catch (e) {
- if (e instanceof UnwrapKeyError) {
- notifyError(
- "Could not unlock account" as any,
- e.message as any,
- );
- } else {
- throw e;
- }
- }
- }}
- >
- <div class="mb-4">
- <Form.InputLine
- label={i18n.str`Password`}
- name="password"
- type="password"
- required
- />
- </div>
- <div class="mt-8">
- <button
- type="submit"
- class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- >
- <i18n.Translate>Unlock</i18n.Translate>
- </button>
- </div>
- </Form.Provider>
+ <div class="mb-4">
+ <InputLine<FormType, "password">
+ label={i18n.str`Password`}
+ name="password"
+ type="password"
+ required
+ handler={form.password}
+ />
+ </div>
+
+ <div class="mt-8">
+ <Button
+ type="submit"
+ handler={unlockHandler}
+ disabled={!unlockHandler}
+ class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ <i18n.Translate>Unlock</i18n.Translate>
+ </Button>
+ </div>
+
</div>
- <button
+ <Button
type="button"
- onClick={() => {
- onRemoveAccount();
- }}
- class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
+ handler={forgetHandler}
+ disabled={!forgetHandler}
+ class="disabled:opacity-50 disabled:cursor-default m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
>
<i18n.Translate>Forget account</i18n.Translate>
- </button>
+ </Button>
</div>
</div>
);
diff --git a/packages/aml-backoffice-ui/src/pages/index.stories.ts b/packages/aml-backoffice-ui/src/pages/index.stories.ts
index afe73227a..f11028de8 100644
--- a/packages/aml-backoffice-ui/src/pages/index.stories.ts
+++ b/packages/aml-backoffice-ui/src/pages/index.stories.ts
@@ -1,3 +1,17 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
export * as a1 from "./ShowConsolidated.stories.js";
-export * as a2 from "./AntiMoneyLaunderingForm.stories.js";
export * as a3 from "./Cases.stories.js";
diff --git a/packages/aml-backoffice-ui/src/route.ts b/packages/aml-backoffice-ui/src/route.ts
deleted file mode 100644
index f515a590a..000000000
--- a/packages/aml-backoffice-ui/src/route.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { createHashHistory } from "history";
-import { ComponentChildren, h as create, createContext, VNode } from "preact";
-import { useContext, useEffect, useState } from "preact/hooks";
-
-type ContextType = {
- onChange: (listener: () => void) => VoidFunction
-}
-const nullChangeListener = { onChange: () => () => { } }
-const Context = createContext<ContextType>(nullChangeListener);
-
-export const usePathChangeContext = (): ContextType => useContext(Context);
-
-export function HashPathProvider({ children }: { children: ComponentChildren }): VNode {
- const history = createHashHistory();
- return create(Context.Provider, { value: { onChange: history.listen }, children }, children)
-}
-
-type PageDefinition<DynamicPart extends Record<string, string>> = {
- pattern: string;
- (params: DynamicPart): string;
-};
-
-function replaceAll(
- pattern: string,
- vars: Record<string, string>,
- values: Record<string, string>,
-): string {
- let result = pattern;
- for (const v in vars) {
- result = result.replace(vars[v], !values[v] ? "" : values[v]);
- }
- return result;
-}
-
-export function pageDefinition<T extends Record<string, string>>(
- pattern: string,
-): PageDefinition<T> {
- const patternParams = pattern.match(/(:[\w?]*)/g);
- if (!patternParams)
- throw Error(
- `page definition pattern ${pattern} doesn't have any parameter`,
- );
-
- const vars = patternParams.reduce((prev, cur) => {
- const pName = cur.match(/(\w+)/g);
-
- //skip things like :? in the path pattern
- if (!pName || !pName[0]) return prev;
- const name = pName[0];
- return { ...prev, [name]: cur };
- }, {} as Record<string, string>);
-
- const f = (values: T): string => replaceAll(pattern, vars, values);
- f.pattern = pattern;
- return f;
-}
-
-export type PageEntry<T = unknown> = T extends Record<string, string>
- ? {
- url: PageDefinition<T>;
- view: (props: T) => VNode;
- name: TranslatedString,
- Icon?: () => VNode,
- }
- : T extends unknown
- ? {
- url: string;
- view: (props: {}) => VNode;
- name: TranslatedString,
- Icon?: () => VNode,
- }
- : never;
-
-export function Router({
- pageList,
- onNotFound,
-}: {
- pageList: Array<PageEntry<any>>;
- onNotFound: () => VNode;
-}): VNode {
- const current = useCurrentLocation(pageList);
- if (current !== undefined) {
- return create(current.page.view, current.values);
- }
- return onNotFound();
-}
-
-type Location = {
- page: PageEntry<any>;
- path: string;
- values: Record<string, string>;
-};
-export function useCurrentLocation(pageList: Array<PageEntry<any>>): Location | undefined {
- const [currentLocation, setCurrentLocation] = useState<Location | null | undefined>(null);
- const path = usePathChangeContext();
- useEffect(() => {
- return path.onChange(() => {
- const result = doSync(window.location.hash, new URLSearchParams(window.location.search), pageList);
- setCurrentLocation(result);
- });
- }, []);
- if (currentLocation === null) {
- return doSync(window.location.hash, new URLSearchParams(window.location.search), pageList);
- }
- return currentLocation;
-}
-
-export function useChangeLocation() {
- const [location, setLocation] = useState(window.location.hash)
- const path = usePathChangeContext()
- useEffect(() => {
- return path.onChange(() => {
- setLocation(window.location.hash)
- });
- }, []);
- return location;
-}
-
-/**
- * Search path in the pageList
- * get the values from the path found
- * add params from searchParams
- *
- * @param path
- * @param params
- */
-export function doSync(path: string, params: URLSearchParams, pageList: Array<PageEntry<any>>): Location | undefined {
- for (let idx = 0; idx < pageList.length; idx++) {
- const page = pageList[idx];
- if (typeof page.url === "string") {
- if (page.url === path) {
- const values: Record<string, string> = {};
- params.forEach((v, k) => {
- values[k] = v;
- });
- return { page, values, path };
- }
- } else {
- const values = doestUrlMatchToRoute(path, page.url.pattern);
- if (values !== undefined) {
- params.forEach((v, k) => {
- values[k] = v;
- });
- return { page, values, path };
- }
- }
- }
- return undefined;
-}
-
-function doestUrlMatchToRoute(
- url: string,
- route: string,
-): undefined | Record<string, string> {
- const paramsPattern = /(?:\?([^#]*))?$/;
- // const paramsPattern = /(?:\?([^#]*))?(#.*)?$/;
- const params = url.match(paramsPattern);
- const urlWithoutParams = url.replace(paramsPattern, "");
-
- const result: Record<string, string> = {};
- if (params && params[1]) {
- const paramList = params[1].split("&");
- for (let i = 0; i < paramList.length; i++) {
- const idx = paramList[i].indexOf("=");
- const name = paramList[i].substring(0, idx);
- const value = paramList[i].substring(idx + 1);
- result[decodeURIComponent(name)] = decodeURIComponent(value);
- }
- }
- const urlSeg = urlWithoutParams.split("/");
- const routeSeg = route.split("/");
- let max = Math.max(urlSeg.length, routeSeg.length);
- for (let i = 0; i < max; i++) {
- if (routeSeg[i] && routeSeg[i].charAt(0) === ":") {
- const param = routeSeg[i].replace(/(^:|[+*?]+$)/g, "");
-
- const flags = (routeSeg[i].match(/[+*?]+$/) || EMPTY)[0] || "";
- const plus = ~flags.indexOf("+");
- const star = ~flags.indexOf("*");
- const val = urlSeg[i] || "";
-
- if (!val && !star && (flags.indexOf("?") < 0 || plus)) {
- return undefined;
- }
- result[param] = decodeURIComponent(val);
- if (plus || star) {
- result[param] = urlSeg.slice(i).map(decodeURIComponent).join("/");
- break;
- }
- } else if (routeSeg[i] !== urlSeg[i]) {
- return undefined;
- }
- }
- return result;
-}
-const EMPTY: Record<string, string> = {};
diff --git a/packages/aml-backoffice-ui/src/settings.json b/packages/aml-backoffice-ui/src/settings.json
new file mode 100644
index 000000000..932202b81
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/settings.json
@@ -0,0 +1,4 @@
+{
+ "backendBaseURL": "http://exchange.taler.test:1180/",
+ "signupEmail": "do-not-contact-me@exchange.taler.test"
+} \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/stories.test.ts b/packages/aml-backoffice-ui/src/stories.test.ts
index eca66cb18..265a2165b 100644
--- a/packages/aml-backoffice-ui/src/stories.test.ts
+++ b/packages/aml-backoffice-ui/src/stories.test.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,15 +19,17 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { TalerExchangeApi, setupI18n } from "@gnu-taler/taler-util";
-import { parseGroupImport } from "@gnu-taler/web-util/browser";
+import {
+ ExchangeApiProviderTesting,
+ ExchangeContextType,
+ parseGroupImport,
+} from "@gnu-taler/web-util/browser";
import * as tests from "@gnu-taler/web-util/testing";
// import * as components from "./components/index.examples.js";
import * as pages from "./pages/index.stories.js";
-import { ComponentChildren, Fragment, VNode, h as create } from "preact";
-import { ExchangeApiContextTesting } from "./context/config.js";
-// import { BackendStateProviderTesting } from "./context/backend.js";
+import { ComponentChildren, VNode, h as create } from "preact";
setupI18n("en", { en: {} });
@@ -48,7 +50,6 @@ describe("All the examples:", () => {
});
});
-
function DefaultTestingContext({
children,
}: {
@@ -61,11 +62,22 @@ function DefaultTestingContext({
name: "ARS",
num_fractional_input_digits: 2,
num_fractional_normal_digits: 2,
- num_fractional_trailing_zero_digits: 2
+ num_fractional_trailing_zero_digits: 2,
},
name: "taler-exchange",
supported_kyc_requirements: [],
version: "asd",
- }
- return create(ExchangeApiContextTesting, { config, children });
+ };
+ const value: ExchangeContextType = {
+ cancelRequest: () => null,
+ config,
+ url: new URL("/", "http://localhost"),
+ hints: [],
+ lib: {
+ exchange: undefined!, //FIXME: mock
+ },
+ onActivity: () => null!,
+ };
+
+ return create(ExchangeApiProviderTesting, { value, children });
}
diff --git a/packages/aml-backoffice-ui/src/stories.tsx b/packages/aml-backoffice-ui/src/stories.tsx
index 1aa6a44ac..9a23d82fa 100644
--- a/packages/aml-backoffice-ui/src/stories.tsx
+++ b/packages/aml-backoffice-ui/src/stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -22,41 +22,57 @@ import { strings } from "./i18n/strings.js";
import * as pages from "./pages/index.stories.js";
-import { renderStories } from "@gnu-taler/web-util/browser";
+import {
+ ExchangeApiProviderTesting,
+ ExchangeContextType,
+ renderStories,
+} from "@gnu-taler/web-util/browser";
+import { TalerExchangeApi } from "@gnu-taler/taler-util";
+import { ComponentChildren, FunctionComponent, VNode, h } from "preact";
import "./scss/main.css";
-import { h, ComponentChildren, FunctionComponent, VNode } from "preact";
-import { ExchangeApiContextTesting } from "./context/config.js";
function main(): void {
renderStories(
{ pages },
{
strings,
- getWrapperForGroup
+ getWrapperForGroup,
},
);
}
function getWrapperForGroup(): FunctionComponent {
return function All({ children }: { children?: ComponentChildren }): VNode {
- return <ExchangeApiContextTesting
- config={{
- currency: "ARS",
- currency_specification: {
- alt_unit_names: {},
- name: "ARS",
- num_fractional_input_digits: 2,
- num_fractional_normal_digits: 2,
- num_fractional_trailing_zero_digits: 2
- },
- name: "taler-exchange",
- supported_kyc_requirements: [],
- version: "asd",
- }}>
- {children}
- </ExchangeApiContextTesting>
- }
+ const config: TalerExchangeApi.ExchangeVersionResponse = {
+ currency: "ARS",
+ currency_specification: {
+ alt_unit_names: {},
+ name: "ARS",
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ },
+ name: "taler-exchange",
+ supported_kyc_requirements: [],
+ version: "asd",
+ };
+ const value: ExchangeContextType = {
+ cancelRequest: () => null,
+ config,
+ url: new URL("/", "http://localhost"),
+ hints: [],
+ lib: {
+ exchange: undefined!, //FIXME: mock
+ },
+ onActivity: () => null!,
+ };
+ return (
+ <ExchangeApiProviderTesting value={value}>
+ {children}
+ </ExchangeApiProviderTesting>
+ );
+ };
}
if (document.readyState === "loading") {
diff --git a/packages/aml-backoffice-ui/src/utils/QR.tsx b/packages/aml-backoffice-ui/src/utils/QR.tsx
index 1dc1712b7..b382348a3 100644
--- a/packages/aml-backoffice-ui/src/utils/QR.tsx
+++ b/packages/aml-backoffice-ui/src/utils/QR.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts b/packages/aml-backoffice-ui/src/utils/converter.ts
deleted file mode 100644
index d2f05ed84..000000000
--- a/packages/aml-backoffice-ui/src/utils/converter.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { AmlExchangeBackend } from "./types.js";
-
-export const amlStateConverter = {
- toStringUI: stringifyAmlState,
- fromStringUI: parseAmlState,
-};
-
-function stringifyAmlState(s: AmlExchangeBackend.AmlState | undefined): string {
- if (s === undefined) return "";
- switch (s) {
- case AmlExchangeBackend.AmlState.normal:
- return "normal";
- case AmlExchangeBackend.AmlState.pending:
- return "pending";
- case AmlExchangeBackend.AmlState.frozen:
- return "frozen";
- }
-}
-
-function parseAmlState(s: string | undefined): AmlExchangeBackend.AmlState {
- switch (s) {
- case "normal":
- return AmlExchangeBackend.AmlState.normal;
- case "pending":
- return AmlExchangeBackend.AmlState.pending;
- case "frozen":
- return AmlExchangeBackend.AmlState.frozen;
- default:
- throw Error(`unknown AML state: ${s}`);
- }
-}
diff --git a/packages/aml-backoffice-ui/src/utils/types.ts b/packages/aml-backoffice-ui/src/utils/types.ts
deleted file mode 100644
index fd70d4e4d..000000000
--- a/packages/aml-backoffice-ui/src/utils/types.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-export namespace AmlExchangeBackend {
- // FIXME: placeholder
- export interface AmlError {
- code: number;
- hint: string;
- }
- export interface AmlDecisionDetails {
- // Array of AML decisions made for this account. Possibly
- // contains only the most recent decision if "history" was
- // not set to 'true'.
- aml_history: AmlDecisionDetail[];
-
- // Array of KYC attributes obtained for this account.
- kyc_attributes: KycDetail[];
- }
-
- type AmlOfficerPublicKeyP = string;
-
- export interface AmlDecisionDetail {
- // What was the justification given?
- justification: string;
-
- // What is the new AML state.
- new_state: Integer;
-
- // When was this decision made?
- decision_time: Timestamp;
-
- // What is the new AML decision threshold (in monthly transaction volume)?
- new_threshold: Amount;
-
- // Who made the decision?
- decider_pub: AmlOfficerPublicKeyP;
- }
- export interface KycDetail {
- // Name of the configuration section that specifies the provider
- // which was used to collect the KYC details
- provider_section: string;
-
- // The collected KYC data. NULL if the attribute data could not
- // be decrypted (internal error of the exchange, likely the
- // attribute key was changed).
- attributes?: Object;
-
- // Time when the KYC data was collected
- collection_time: Timestamp;
-
- // Time when the validity of the KYC data will expire
- expiration_time: Timestamp;
- }
-
- interface Timestamp {
- // Seconds since epoch, or the special
- // value "never" to represent an event that will
- // never happen.
- t_s: number | "never";
- }
-
- type PaytoHash = string;
- type Integer = number;
- type Amount = string;
- // EdDSA signatures are transmitted as 64-bytes base32
- // binary-encoded objects with just the R and S values (base32_ binary-only).
- type EddsaSignature = string;
-
- export interface AmlRecords {
- // Array of AML records matching the query.
- records: AmlRecord[];
- }
-
- interface AmlRecord {
- // Which payto-address is this record about.
- // Identifies a GNU Taler wallet or an affected bank account.
- h_payto: PaytoHash;
-
- // What is the current AML state.
- current_state: AmlState;
-
- // Monthly transaction threshold before a review will be triggered
- threshold: Amount;
-
- // RowID of the record.
- rowid: Integer;
- }
-
- export enum AmlState {
- normal = 0,
- pending = 1,
- frozen = 2,
- }
-
-
- export interface AmlDecision {
-
- // Human-readable justification for the decision.
- justification: string;
-
- // At what monthly transaction volume should the
- // decision be automatically reviewed?
- new_threshold: Amount;
-
- // Which payto-address is the decision about?
- // Identifies a GNU Taler wallet or an affected bank account.
- h_payto: PaytoHash;
-
- // What is the new AML state (e.g. frozen, unfrozen, etc.)
- // Numerical values are defined in AmlDecisionState.
- new_state: Integer;
-
- // Signature by the AML officer over a
- // TALER_MasterAmlOfficerStatusPS.
- // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
- officer_sig: EddsaSignature;
-
- // When was the decision made?
- decision_time: Timestamp;
-
- // Optional argument to impose new KYC requirements
- // that the customer has to satisfy to unblock transactions.
- kyc_requirements?: string[];
- }
-
-
-}
diff --git a/packages/anastasis-cli/package.json b/packages/anastasis-cli/package.json
index b0e26fae3..5a9d6abea 100644
--- a/packages/anastasis-cli/package.json
+++ b/packages/anastasis-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/anastasis-cli",
- "version": "0.0.1",
+ "version": "0.10.7",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json
index f551a41f8..576acc988 100644
--- a/packages/anastasis-core/package.json
+++ b/packages/anastasis-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/anastasis-core",
- "version": "0.0.2",
+ "version": "0.10.7",
"description": "",
"main": "./lib/index.js",
"module": "./lib/index.js",
diff --git a/packages/anastasis-core/src/anastasis-data.ts b/packages/anastasis-core/src/anastasis-data.ts
index d69bb319b..9cbf5f594 100644
--- a/packages/anastasis-core/src/anastasis-data.ts
+++ b/packages/anastasis-core/src/anastasis-data.ts
@@ -11,14 +11,10 @@ export const anastasisData = {
url: "https://v1.anastasis.taler.net/",
name: "Bern University of Applied Sciences, Switzerland",
},
- {
- url: "https://v1.anastasis.codeblau.de/",
- name: "Codeblau GmbH, Germany",
- },
- // {
- // url: "https://v1.anastasis.openw3b.org/",
- // name: "Openw3b Foundation, India",
- // },
+// {
+// url: "https://v1.anastasis.codeblau.de/",
+// name: "Codeblau GmbH, Germany",
+// },
{
url: "https://v1.anastasis.lu/",
name: "Anastasis SARL, Luxembourg",
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts
index 9a774d0ff..05fa4a49f 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -43,7 +43,7 @@ import {
URL,
j2s,
} from "@gnu-taler/taler-util";
-import { HttpResponse, createPlatformHttpLib } from "@gnu-taler/taler-util/http";
+import { HttpResponse } from "@gnu-taler/taler-util/http";
import { anastasisData } from "./anastasis-data.js";
import {
codecForChallengeInstructionMessage,
@@ -137,10 +137,6 @@ export * from "./reducer-types.js";
export * as validators from "./validators.js";
export * from "./challenge-feedback-types.js";
-const httpLib = createPlatformHttpLib({
- enableThrottling: false,
-});
-
const logger = new Logger("anastasis-core:index.ts");
const ANASTASIS_HTTP_HEADER_POLICY_META_DATA = "Anastasis-Policy-Meta-Data";
@@ -283,17 +279,22 @@ async function getProviderInfo(
providerBaseUrl: string,
): Promise<AuthenticationProviderStatus> {
// FIXME: Use a reasonable timeout here.
- let resp: HttpResponse;
+ let resp: Response;
try {
- resp = await httpLib.fetch(new URL("config", providerBaseUrl).href);
+ resp = await fetch(new URL("config", providerBaseUrl).href);
} catch (e) {
+ console.warn(
+ "Encountered an HTTP error whilst trying to get the provider's config: ",
+ e,
+ );
return {
status: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
- hint: "request to provider failed",
+ hint: "request to anastasis provider failed",
};
}
- if (resp.status !== 200) {
+ if (!resp.ok) {
+ console.warn("Got bad response code whilst getting provider config", resp);
return {
status: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
@@ -555,7 +556,7 @@ async function uploadSecret(
// FIXME: Get this from the params
reqUrl.searchParams.set("timeout_ms", "500");
}
- const resp = await httpLib.fetch(reqUrl.href, {
+ const resp = await fetch(reqUrl.href, {
method: "POST",
headers: {
"content-type": "application/json",
@@ -645,11 +646,11 @@ async function uploadSecret(
reqUrl.searchParams.set("timeout_ms", "500");
}
logger.info(`uploading policy to ${prov.provider_url}`);
- const resp = await httpLib.fetch(reqUrl.href, {
+ const resp = await fetch(reqUrl.href, {
method: "POST",
headers: {
"Anastasis-Policy-Signature": encodeCrock(sig),
- "If-None-Match": encodeCrock(bodyHash),
+ "If-None-Match": JSON.stringify(encodeCrock(bodyHash)),
[ANASTASIS_HTTP_HEADER_POLICY_META_DATA]: metadataEnc,
...(paySecret
? {
@@ -756,14 +757,14 @@ async function downloadPolicyFromProvider(
const acctKeypair = accountKeypairDerive(userId);
const reqUrl = new URL(`policy/${acctKeypair.pub}`, providerUrl);
reqUrl.searchParams.set("version", `${version}`);
- const resp = await httpLib.fetch(reqUrl.href);
+ const resp = await fetch(reqUrl.href);
if (resp.status !== 200) {
logger.info(
`Could not download policy from provider ${providerUrl}, status ${resp.status}`,
);
return undefined;
}
- const body = await resp.bytes();
+ const body = await resp.arrayBuffer();
const bodyDecrypted = await decryptRecoveryDocument(
userId,
encodeCrock(body),
@@ -980,10 +981,10 @@ async function requestTruth(
const hresp = await getResponseHash(truth, solveRequest);
- let resp: HttpResponse;
+ let resp: Response;
try {
- resp = await httpLib.fetch(url.href, {
+ resp = await fetch(url.href, {
method: "POST",
headers: {
Accept: "application/json",
@@ -1021,7 +1022,7 @@ async function requestTruth(
truth.provider_salt,
);
- const respBody = new Uint8Array(await resp.bytes());
+ const respBody = new Uint8Array(await resp.arrayBuffer());
const keyShare = await decryptKeyShare(
encodeCrock(respBody),
userId,
@@ -1137,10 +1138,10 @@ async function selectChallenge(
}
}
- let resp: HttpResponse;
+ let resp: Response;
try {
- resp = await httpLib.fetch(url.href, {
+ resp = await fetch(url.href, {
method: "POST",
headers: {
Accept: "application/json",
@@ -1858,7 +1859,7 @@ export async function discoverPolicies(
);
const acctKeypair = accountKeypairDerive(userId);
const reqUrl = new URL(`policy/${acctKeypair.pub}/meta`, providerUrl);
- const resp = await httpLib.fetch(reqUrl.href);
+ const resp = await fetch(reqUrl.href);
if (resp.status !== 200) {
logger.warn(`Could not fetch policy metadate from ${reqUrl.href}`);
continue;
diff --git a/packages/anastasis-core/tsconfig.json b/packages/anastasis-core/tsconfig.json
index a12f2e641..e463201e7 100644
--- a/packages/anastasis-core/tsconfig.json
+++ b/packages/anastasis-core/tsconfig.json
@@ -6,7 +6,7 @@
"module": "Node16",
"moduleResolution": "Node16",
"sourceMap": true,
- "lib": ["ES2020"],
+ "lib": ["ES2020", "DOM"],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
index c1c2925a2..108b1476e 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/anastasis-webui",
- "version": "0.2.99",
+ "version": "0.10.7",
"license": "MIT",
"type": "module",
"scripts": {
diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx
index 3dac73e04..31bc3c7a7 100644
--- a/packages/anastasis-webui/src/components/menu/SideBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx
@@ -29,7 +29,10 @@ interface Props {
}
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
-const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
+const GIT_HASH =
+ typeof __GIT_HASH__ !== "undefined"
+ ? __GIT_HASH__.substring(0, 7)
+ : undefined;
const VERSION_WITH_HASH = GIT_HASH ? `${VERSION}-${GIT_HASH}` : VERSION;
export function Sidebar({ mobile }: Props): VNode {
diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
index fc8c4cf6c..fcc380775 100644
--- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
+++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
@@ -303,7 +303,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
},
});
} catch (e) {
- throw Error("could not restore the state");
+ throw new Error("could not restore the state");
}
},
async discoverStart(): Promise<void> {
@@ -399,7 +399,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
}
class ReducerTxImpl implements ReducerTransactionHandle {
- constructor(public transactionState: ReducerState) { }
+ constructor(public transactionState: ReducerState) {}
async transition(action: string, args: any): Promise<ReducerState> {
let s: ReducerState;
if (remoteReducer) {
@@ -410,7 +410,7 @@ class ReducerTxImpl implements ReducerTransactionHandle {
this.transactionState = s;
// Abort transaction as soon as we transition into an error state.
if (this.transactionState.reducer_type === "error") {
- throw Error("transition resulted in error");
+ throw new Error("transition resulted in error");
}
return this.transactionState;
}
diff --git a/packages/anastasis-webui/src/index.html b/packages/anastasis-webui/src/index.html
index 90a795ae3..d64b627e4 100644
--- a/packages/anastasis-webui/src/index.html
+++ b/packages/anastasis-webui/src/index.html
@@ -32,7 +32,7 @@
/>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<title>Anastasis</title>
- <!-- Entry point for the demobank SPA. -->
+ <!-- Entry point for the SPA. -->
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
diff --git a/packages/anastasis-webui/src/index.ts b/packages/anastasis-webui/src/index.ts
index d7b2164ab..f614e4f54 100644
--- a/packages/anastasis-webui/src/index.ts
+++ b/packages/anastasis-webui/src/index.ts
@@ -22,7 +22,7 @@ function main(): void {
try {
const container = document.getElementById("container");
if (!container) {
- throw Error("container not found, can't mount page contents");
+ throw new Error("container not found, can't mount page contents");
}
render(h(App, {}), container);
} catch (e) {
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts
index 0ab275f54..ed8301d65 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts
@@ -24,7 +24,7 @@ import { WithoutProviderType, WithProviderType } from "./views.js";
export type AuthProvByStatusMap = Record<
AuthenticationProviderStatus["status"],
(AuthenticationProviderStatus & { url: string })[]
->
+>;
export type State = NoReducer | InvalidState | WithType | WithoutType;
@@ -63,42 +63,69 @@ const map: StateViewMap<State> = {
"without-type": WithoutProviderType,
};
-export default compose("AddingProviderScreen", useComponentState, map)
-
+export default compose("AddingProviderScreen", useComponentState, map);
+const providerResponseCache = new Map<string, any>(); // `any` is the return type of res.json()
export async function testProvider(
url: string,
expectedMethodType?: string,
): Promise<void> {
+ const testFatalPrefix = `Encountered a fatal error whilst testing the provider ${url}`;
+ let configUrl = "";
try {
- const response = await fetch(new URL("config", url).href);
- const json = await response.json().catch((d) => ({}));
- if (!("methods" in json) || !Array.isArray(json.methods)) {
- throw Error(
- "This provider doesn't have authentication method. Check the provider URL",
- );
- }
- if (!expectedMethodType) {
- return;
- }
- let found = false;
- for (let i = 0; i < json.methods.length && !found; i++) {
- found = json.methods[i].type === expectedMethodType;
- }
- if (!found) {
- throw Error(
- `This provider does not support authentication method ${expectedMethodType}`,
- );
- }
+ configUrl = new URL("config", url).href;
+ } catch (error) {
+ throw new Error(`${testFatalPrefix}: Invalid Provider URL: ${url}
+Error: ${error}`);
+ }
+ // TODO: look into using core.getProviderInfo :)
+ const providerHasUrl = providerResponseCache.has(url);
+ const json = providerHasUrl
+ ? providerResponseCache.get(url)
+ : await fetch(configUrl)
+ .catch((error) => {
+ throw new Error(`${testFatalPrefix}: Could not connect: ${error}
+Please check the URL.`);
+ })
+ .then(async (response) => {
+ if (!response.ok)
+ throw new Error(
+ `${testFatalPrefix}: The server ${response.url} responded with a non-2xx response.`,
+ );
+ try {
+ return await response.json();
+ } catch (error) {
+ throw new Error(
+ `${testFatalPrefix}: The server responded with malformed JSON.\nError: ${error}`,
+ );
+ }
+ });
+ if (typeof json !== "object")
+ throw new Error(
+ `${testFatalPrefix}: Did not get an object after decoding.`,
+ );
+ if (!("name" in json) || json.name !== "anastasis") {
+ throw new Error(
+ `${testFatalPrefix}: The provider does not appear to be an Anastasis provider. Please check the provider's URL.`,
+ );
+ }
+ if (!("methods" in json) || !Array.isArray(json.methods)) {
+ throw new Error(
+ "This provider doesn't have authentication method. Please check the provider's URL and ensure it is properly configured.",
+ );
+ }
+ if (!providerHasUrl) providerResponseCache.set(url, json);
+ if (!expectedMethodType) {
return;
- } catch (e) {
- console.log("ERROR testProvider", e);
- const error =
- e instanceof Error
- ? Error(
- `There was an error testing this provider, try another one. ${e.message}`,
- )
- : Error(`There was an error testing this provider, try another one.`);
- throw error;
}
+ let found = false;
+ for (let i = 0; i < json.methods.length && !found; i++) {
+ found = json.methods[i].type === expectedMethodType;
+ }
+ if (!found) {
+ throw new Error(
+ `${testFatalPrefix}: This provider does not support authentication method ${expectedMethodType}`,
+ );
+ }
+ return;
}
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts
index f80f1c464..30e4d750d 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts
@@ -76,14 +76,23 @@ export default function useComponentState({
useEffect(() => {
if (timeout.current) clearTimeout(timeout.current);
timeout.current = setTimeout(async () => {
- const url = providerURL.endsWith("/") ? providerURL : providerURL + "/";
- if (!providerURL || authProviders.includes(url)) return;
+ let url = providerURL;
+ if (!url || authProviders.includes(url)) return;
+ if (url && !url.match(/^(https?:)\/\/.+\/(?:config)?$/iu))
+ return setError(
+ "Malformed URL: Must be an HTTP(S) URL ending with a /",
+ );
+ if (url.endsWith("/config")) url = url.substring(0, url.length - 6);
try {
setTesting(true);
await testProvider(url, providerType);
setError("");
} catch (e) {
if (e instanceof Error) setError(e.message);
+ else
+ throw new Error(
+ `Unexpected Error Type: ${typeof e} - Cannot handle. Error: ${e}`,
+ );
}
setTesting(false);
}, 200);
@@ -114,11 +123,12 @@ export default function useComponentState({
let errors = !providerURL ? "Add provider URL" : undefined;
let url: string | undefined;
- try {
- url = new URL("", providerURL).href;
- } catch {
- errors = "Check the URL";
- }
+ // We'll validate it in testProvider & via a regex above - there's no need in this :)
+ // try {
+ // url = new URL("", providerURL).href;
+ // } catch {
+ // errors = "Check the URL";
+ // }
const _url = url;
if (!!error && !errors) {
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
index 19557a12f..00a42a949 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx
@@ -121,13 +121,13 @@ export function WithoutProviderType(props: WithoutType): VNode {
<div class="container">
<TextInput
label="Provider URL"
- placeholder="https://provider.com"
+ placeholder="https://provider.com/"
grabFocus
error={props.errors}
bind={[props.providerURL, props.setProviderURL]}
/>
</div>
- <p class="block">Example: https://kudos.demo.anastasis.lu</p>
+ <p class="block">Example: https://kudos.demo.anastasis.lu/</p>
{props.testing && <p class="has-text-info">Testing</p>}
<div
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
index 228186a2d..1f8cea7aa 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
@@ -97,13 +97,11 @@ export function AttributeEntryScreen(): VNode {
function saveAsPDF(): void {
const printWindow = window.open("", "", "height=400,width=800");
const divContents = document.getElementById("printThis");
- const styleContents = document.getElementById("style-id");
- if (!printWindow || !divContents || !styleContents) return;
+ if (!printWindow || !divContents) return;
printWindow.document.write(
- "<html><head><title>Anastasis Recovery Document</title><style>",
+ `<html><head><link rel="stylesheet" href="index.css" /><title>Anastasis Recovery Document</title><style>`,
);
- printWindow.document.write(styleContents.innerHTML);
printWindow.document.write("</style></head><body>&nbsp;</body></html>");
printWindow.document.close();
printWindow.document.body.appendChild(divContents.cloneNode(true));
@@ -132,6 +130,7 @@ export function AttributeEntryScreen(): VNode {
secret will be safely stored. If you forget what you have entered or
if there is a misspell you will be unable to recover your secret.
<p>
+ {/* TODO: make this actually work reliably cross-browser lol (opens about:blank for me) */}
<a onClick={saveAsPDF}>Save the personal information as PDF</a>
</p>
</ConfirmModal>
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
index 62ac410a2..f528bc207 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
@@ -58,9 +58,14 @@ export function RecoveryFinishedScreen(): VNode {
const secret = bytesToString(decodeCrock(encodedSecret.value));
const plainText =
encodedSecret.value.length < 1000 && encodedSecret.mime === "text/plain";
- const contentURI = !plainText
- ? secret
- : `data:${encodedSecret.mime},${secret}`;
+
+ let [uri, setUri] = useState(`data:${encodedSecret.mime},${secret}`);
+ fetch(`data:${encodedSecret.mime},${secret}`) // TODO: look into using new Blob
+ .then((v) => v.blob())
+ .then((blob) => URL.createObjectURL(blob))
+ .then((newUri) => {
+ setUri(newUri);
+ });
return (
<AnastasisClientFrame title="Recovery Success" hideNav>
<h2 class="subtitle">Your secret was recovered</h2>
@@ -87,7 +92,7 @@ export function RecoveryFinishedScreen(): VNode {
download={
encodedSecret.filename ? encodedSecret.filename : "secret.file"
}
- href={contentURI}
+ href={uri}
>
<div class="icon is-small ">
<i class="mdi mdi-download" />
diff --git a/packages/auditor-backoffice-ui/package.json b/packages/auditor-backoffice-ui/package.json
index 33d397f93..776c179b4 100644
--- a/packages/auditor-backoffice-ui/package.json
+++ b/packages/auditor-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/auditor-backoffice-ui",
- "version": "0.9.3-dev.27",
+ "version": "0.10.7",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
@@ -47,7 +47,7 @@
},
"devDependencies": {
"@creativebulma/bulma-tooltip": "^1.2.0",
- "@gnu-taler/pogen": "^0.0.5",
+ "@gnu-taler/pogen": "workspace:*",
"@types/chai": "^4.3.0",
"@types/history": "^4.7.8",
"@types/mocha": "^8.2.3",
diff --git a/packages/auditor-backoffice-ui/src/hooks/testing.tsx b/packages/auditor-backoffice-ui/src/hooks/testing.tsx
index a0ba59b2e..7955f832a 100644
--- a/packages/auditor-backoffice-ui/src/hooks/testing.tsx
+++ b/packages/auditor-backoffice-ui/src/hooks/testing.tsx
@@ -145,7 +145,7 @@ export class ApiMockEnvironment extends MockEnvironment {
}
const bankCore = new TalerCoreBankHttpClient("http://localhost", mockHttpClient)
const bankIntegration = new TalerBankIntegrationHttpClient(bankCore.getIntegrationAPI().href, mockHttpClient)
- const bankRevenue = new TalerRevenueHttpClient(bankCore.getRevenueAPI("a").href, "a", mockHttpClient)
+ const bankRevenue = new TalerRevenueHttpClient(bankCore.getRevenueAPI("a").href, mockHttpClient)
const bankWire = new TalerWireGatewayHttpClient(bankCore.getWireGatewayAPI("b").href, "b", mockHttpClient)
return (
diff --git a/packages/auditor-backoffice-ui/src/index.html b/packages/auditor-backoffice-ui/src/index.html
index d79bdf130..c73dd1936 100644
--- a/packages/auditor-backoffice-ui/src/index.html
+++ b/packages/auditor-backoffice-ui/src/index.html
@@ -35,7 +35,7 @@
<title>Auditor Backoffice</title>
<!-- Optional customization script. -->
<script src="auditor-backoffice-ui-settings.js"></script>
- <!-- Entry point for the demobank SPA. -->
+ <!-- Entry point for the SPA. -->
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
diff --git a/packages/bank-ui/README.md b/packages/bank-ui/README.md
index 3d2e991ff..4275cce57 100644
--- a/packages/bank-ui/README.md
+++ b/packages/bank-ui/README.md
@@ -1,4 +1,4 @@
-# Taler Demobank UI
+# Taler Bank UI
Web-based user interface for the libeufin bank ui.
diff --git a/packages/bank-ui/package.json b/packages/bank-ui/package.json
index c25decf8d..f06905a93 100644
--- a/packages/bank-ui/package.json
+++ b/packages/bank-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/bank-ui",
- "version": "0.9.3-dev.29",
+ "version": "0.10.7",
"license": "AGPL-3.0-OR-LATER",
"type": "module",
"scripts": {
@@ -31,7 +31,7 @@
"@typescript-eslint/parser": "^6.19.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.33.2",
- "@gnu-taler/pogen": "^0.0.5",
+ "@gnu-taler/pogen": "workspace:*",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"@types/chai": "^4.3.0",
diff --git a/packages/bank-ui/postcss.config.js b/packages/bank-ui/postcss.config.js
index 2e7af2b7f..c9a60a43c 100644
--- a/packages/bank-ui/postcss.config.js
+++ b/packages/bank-ui/postcss.config.js
@@ -1,3 +1,18 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
export default {
plugins: {
tailwindcss: {},
diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx
index 3ec5f0c77..380b267a2 100644
--- a/packages/bank-ui/src/Routing.tsx
+++ b/packages/bank-ui/src/Routing.tsx
@@ -31,6 +31,7 @@ import {
HttpStatusCode,
TranslatedString,
assertUnreachable,
+ createRFC8959AccessTokenEncoded
} from "@gnu-taler/taler-util";
import { useEffect } from "preact/hooks";
import { useSessionState } from "./hooks/session.js";
@@ -84,7 +85,7 @@ const publicPages = {
register: urlPattern(/\/register/, () => "#/register"),
publicAccounts: urlPattern(/\/public-accounts/, () => "#/public-accounts"),
operationDetails: urlPattern<{ wopid: string }>(
- /\/operation\/(?<wopid>[a-zA-Z0-9]+)/,
+ /\/operation\/(?<wopid>[a-zA-Z0-9-]+)/,
({ wopid }) => `#/operation/${wopid}`,
),
solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"),
@@ -113,13 +114,15 @@ function PublicRounting({
async function doAutomaticLogin(username: string, password: string) {
await handleError(async () => {
- const resp = await lib.auth(username).createAccessTokenBasic(username, password, {
- scope: "readwrite",
- duration: { d_us: "forever" },
- refreshable: true,
- });
+ const resp = await lib
+ .auth(username)
+ .createAccessTokenBasic(username, password, {
+ scope: "readwrite",
+ duration: { d_us: "forever" },
+ refreshable: true,
+ });
if (resp.type === "ok") {
- onLoggedUser(username, resp.body.access_token);
+ onLoggedUser(username, createRFC8959AccessTokenEncoded(resp.body.access_token));
} else {
switch (resp.case) {
case HttpStatusCode.Unauthorized:
@@ -392,6 +395,9 @@ function PrivateRouting({
routeMyAccountDetails={privatePages.myAccountDetails}
routeMyAccountPassword={privatePages.myAccountPassword}
routeConversionConfig={privatePages.conversionConfig}
+ onCashout={() =>
+ navigateTo(privatePages.home.url({}))
+ }
onAuthorizationRequired={() =>
navigateTo(privatePages.solveSecondFactor.url({}))
}
@@ -459,6 +465,9 @@ function PrivateRouting({
routeMyAccountDetails={privatePages.myAccountDetails}
routeMyAccountPassword={privatePages.myAccountPassword}
routeConversionConfig={privatePages.conversionConfig}
+ onCashout={() =>
+ navigateTo(privatePages.home.url({}))
+ }
onAuthorizationRequired={() =>
navigateTo(privatePages.solveSecondFactor.url({}))
}
@@ -513,6 +522,7 @@ function PrivateRouting({
onAuthorizationRequired={() =>
navigateTo(privatePages.solveSecondFactor.url({}))
}
+ onCashout={() => navigateTo(privatePages.home.url({}))}
routeClose={privatePages.home}
/>
);
diff --git a/packages/bank-ui/src/app.tsx b/packages/bank-ui/src/app.tsx
index 434c132ed..1ea8c69ca 100644
--- a/packages/bank-ui/src/app.tsx
+++ b/packages/bank-ui/src/app.tsx
@@ -23,24 +23,35 @@ import {
getGlobalLogLevel,
setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
-import { BankApiProvider, BrowserHashNavigationProvider, Loading, TalerWalletIntegrationBrowserProvider, TranslationProvider } from "@gnu-taler/web-util/browser";
+import {
+ BankApiProvider,
+ BrowserHashNavigationProvider,
+ Loading,
+ TalerWalletIntegrationBrowserProvider,
+ TranslationProvider,
+} from "@gnu-taler/web-util/browser";
import { h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { SWRConfig } from "swr";
import { Routing } from "./Routing.js";
-// import { BankCoreApiProvider } from "./context/config.js";
-// import { BrowserHashNavigationProvider } from "./context/navigation.js";
import { SettingsProvider } from "./context/settings.js";
-// import { TalerWalletIntegrationBrowserProvider } from "./context/wallet-integration.js";
import { strings } from "./i18n/strings.js";
import { BankFrame } from "./pages/BankFrame.js";
-import { BankUiSettings, fetchSettings } from "./settings.js";
-import { revalidateAccountDetails, revalidatePublicAccounts, revalidateTransactions } from "./hooks/account.js";
-import { revalidateBusinessAccounts, revalidateCashouts, revalidateConversionInfo } from "./hooks/regional.js";
+import { UiSettings, fetchSettings } from "./settings.js";
+import {
+ revalidateAccountDetails,
+ revalidatePublicAccounts,
+ revalidateTransactions,
+} from "./hooks/account.js";
+import {
+ revalidateBusinessAccounts,
+ revalidateCashouts,
+ revalidateConversionInfo,
+} from "./hooks/regional.js";
const WITH_LOCAL_STORAGE_CACHE = false;
export function App() {
- const [settings, setSettings] = useState<BankUiSettings>();
+ const [settings, setSettings] = useState<UiSettings>();
useEffect(() => {
fetchSettings(setSettings);
}, []);
@@ -56,10 +67,14 @@ export function App() {
de: strings["de"].completeness,
}}
>
- <BankApiProvider baseUrl={new URL("/", baseUrl)} frameOnError={BankFrame} evictors={{
- bank: evictBankSwrCache,
- conversion: evictConversionSwrCache,
- }}>
+ <BankApiProvider
+ baseUrl={new URL("/", baseUrl)}
+ frameOnError={BankFrame}
+ evictors={{
+ bank: evictBankSwrCache,
+ conversion: evictConversionSwrCache,
+ }}
+ >
<SWRConfig
value={{
provider: WITH_LOCAL_STORAGE_CACHE
@@ -145,7 +160,6 @@ function getInitialBackendBaseURL(
}
}
-
const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
async notifySuccess(op) {
switch (op) {
@@ -203,15 +217,15 @@ const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
};
const evictConversionSwrCache: CacheEvictor<TalerBankConversionCacheEviction> =
-{
- async notifySuccess(op) {
- switch (op) {
- case TalerBankConversionCacheEviction.UPDATE_RATE: {
- await revalidateConversionInfo();
- return;
+ {
+ async notifySuccess(op) {
+ switch (op) {
+ case TalerBankConversionCacheEviction.UPDATE_RATE: {
+ await revalidateConversionInfo();
+ return;
+ }
+ default:
+ assertUnreachable(op);
}
- default:
- assertUnreachable(op);
- }
- },
-};
+ },
+ };
diff --git a/packages/bank-ui/src/components/Transactions/index.ts b/packages/bank-ui/src/components/Transactions/index.ts
index 2f68b2ded..6fccfcd79 100644
--- a/packages/bank-ui/src/components/Transactions/index.ts
+++ b/packages/bank-ui/src/components/Transactions/index.ts
@@ -24,12 +24,12 @@ import { RouteDefinition } from "@gnu-taler/web-util/browser";
export interface Props {
account: string;
routeCreateWireTransfer:
- | RouteDefinition<{
- account?: string;
- subject?: string;
- amount?: string;
- }>
- | undefined;
+ | RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>
+ | undefined;
}
export type State = State.Loading | State.LoadingUriError | State.Ready;
@@ -52,12 +52,12 @@ export namespace State {
status: "ready";
error: undefined;
routeCreateWireTransfer:
- | RouteDefinition<{
- account?: string;
- subject?: string;
- amount?: string;
- }>
- | undefined;
+ | RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>
+ | undefined;
transactions: Transaction[];
onGoStart?: () => void;
onGoNext?: () => void;
diff --git a/packages/bank-ui/src/components/Transactions/state.ts b/packages/bank-ui/src/components/Transactions/state.ts
index 4e4552a82..ce6338e57 100644
--- a/packages/bank-ui/src/components/Transactions/state.ts
+++ b/packages/bank-ui/src/components/Transactions/state.ts
@@ -17,7 +17,9 @@
import {
AbsoluteTime,
Amounts,
+ HttpStatusCode,
TalerError,
+ assertUnreachable,
parsePaytoUri,
} from "@gnu-taler/taler-util";
import { useTransactions } from "../../hooks/account.js";
@@ -27,21 +29,27 @@ export function useComponentState({
account,
routeCreateWireTransfer,
}: Props): State {
- const txResult = useTransactions(account);
- if (!txResult) {
+ const result = useTransactions(account);
+ if (!result) {
return {
status: "loading",
error: undefined,
};
}
- if (txResult instanceof TalerError) {
+ if (result instanceof TalerError) {
return {
status: "loading-error",
- error: txResult,
+ error: result,
+ };
+ }
+ if (result.type === "fail") {
+ return {
+ status: "loading",
+ error: undefined,
};
}
- const transactions = txResult.result
+ const transactions = result.body
.map((tx) => {
const negative = tx.direction === "debit";
const cp = parsePaytoUri(
@@ -76,7 +84,7 @@ export function useComponentState({
error: undefined,
routeCreateWireTransfer,
transactions,
- onGoNext: txResult.isLastPage ? undefined : txResult.loadNext,
- onGoStart: txResult.isFirstPage ? undefined : txResult.loadFirst,
+ onGoNext: result.isLastPage ? undefined : result.loadNext,
+ onGoStart: result.isFirstPage ? undefined : result.loadFirst,
};
}
diff --git a/packages/bank-ui/src/components/Transactions/views.tsx b/packages/bank-ui/src/components/Transactions/views.tsx
index ebce00a2a..10d63e6af 100644
--- a/packages/bank-ui/src/components/Transactions/views.tsx
+++ b/packages/bank-ui/src/components/Transactions/views.tsx
@@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Attention, useBankCoreApiContext, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ Attention,
+ useBankCoreApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
@@ -119,7 +123,7 @@ export function ReadyView({
<Time
format="HH:mm:ss"
timestamp={item.when}
- // relative={Duration.fromSpec({ days: 1 })}
+ // relative={Duration.fromSpec({ days: 1 })}
/>
</div>
<dl class="font-normal sm:hidden">
diff --git a/packages/bank-ui/src/context/config.ts b/packages/bank-ui/src/context/config.ts
deleted file mode 100644
index 86b6df5f3..000000000
--- a/packages/bank-ui/src/context/config.ts
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- LibtoolVersion,
- ObservableHttpClientLibrary,
- TalerAuthenticationHttpClient,
- TalerBankConversionCacheEviction,
- TalerBankConversionHttpClient,
- TalerCoreBankCacheEviction,
- TalerCoreBankHttpClient,
- TalerCorebankApi,
- TalerError,
- assertUnreachable,
- CacheEvictor,
- ObservabilityEvent,
-} from "@gnu-taler/taler-util";
-import {
- BrowserFetchHttpLib,
- ErrorLoading,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import {
- ComponentChildren,
- FunctionComponent,
- VNode,
- createContext,
- h,
-} from "preact";
-import { useContext, useEffect, useState } from "preact/hooks";
-import {
- revalidateAccountDetails,
- revalidatePublicAccounts,
- revalidateTransactions,
-} from "../hooks/account.js";
-import {
- revalidateBusinessAccounts,
- revalidateCashouts,
- revalidateConversionInfo,
-} from "../hooks/regional.js";
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-export type Type = {
- url: URL;
- config: TalerCorebankApi.Config;
- bank: TalerCoreBankHttpClient;
- conversion: TalerBankConversionHttpClient;
- authenticator: (user: string) => TalerAuthenticationHttpClient;
- hints: VersionHint[];
- onBackendActivity: (fn: Listener) => Unsuscriber;
- cancelRequest: (eventId: string) => void;
-};
-
-// FIXME: below
-// @ts-expect-error default value to undefined, should it be another thing?
-const Context = createContext<Type>(undefined);
-
-export const useBankCoreApiContext = (): Type => useContext(Context);
-
-export enum VersionHint {
- /**
- * when this flag is on, server is running an old version with cashout before implementing 2fa API
- */
- CASHOUT_BEFORE_2FA,
-}
-
-const observers = new Array<(e: ObservabilityEvent) => void>();
-type Listener = (e: ObservabilityEvent) => void;
-type Unsuscriber = () => void;
-
-const activity = Object.freeze({
- notify: (data: ObservabilityEvent) =>
- observers.forEach((observer) => observer(data)),
- subscribe: (func: Listener): Unsuscriber => {
- observers.push(func);
- return () => {
- observers.forEach((observer, index) => {
- if (observer === func) {
- observers.splice(index, 1);
- }
- });
- };
- },
-});
-
-export type ConfigResult =
- | undefined
- | { type: "ok"; config: TalerCorebankApi.Config; hints: VersionHint[] }
- | { type: "incompatible"; result: TalerCorebankApi.Config; supported: string }
- | { type: "error"; error: TalerError };
-
-export const BankCoreApiProvider = ({
- baseUrl,
- children,
- frameOnError,
-}: {
- baseUrl: string;
- children: ComponentChildren;
- frameOnError: FunctionComponent<{ children: ComponentChildren }>;
-}): VNode => {
- const [checked, setChecked] = useState<ConfigResult>();
- const { i18n } = useTranslationContext();
-
- const { bankClient, conversionClient, authClient, cancelRequest } =
- buildApiClient(new URL(baseUrl));
-
- useEffect(() => {
- bankClient
- .getConfig()
- .then((resp) => {
- if (bankClient.isCompatible(resp.body.version)) {
- setChecked({ type: "ok", config: resp.body, hints: [] });
- } else {
- // this API supports version 3.0.3
- const compare = LibtoolVersion.compare("3:0:3", resp.body.version);
- if (compare?.compatible ?? false) {
- setChecked({
- type: "ok",
- config: resp.body,
- hints: [VersionHint.CASHOUT_BEFORE_2FA],
- });
- } else {
- setChecked({
- type: "incompatible",
- result: resp.body,
- supported: bankClient.PROTOCOL_VERSION,
- });
- }
- }
- })
- .catch((error: unknown) => {
- if (error instanceof TalerError) {
- setChecked({ type: "error", error });
- }
- });
- }, []);
-
- if (checked === undefined) {
- return h(frameOnError, { children: h("div", {}, "loading...") });
- }
- if (checked.type === "error") {
- return h(frameOnError, {
- children: h(ErrorLoading, { error: checked.error, showDetail: true }),
- });
- }
- if (checked.type === "incompatible") {
- return h(frameOnError, {
- children: h(
- "div",
- {},
- i18n.str`The bank backend is not supported. Supported version "${checked.supported}", server version "${checked.result.version}"`,
- ),
- });
- }
- const value: Type = {
- url: new URL(bankClient.baseUrl),
- config: checked.config,
- bank: bankClient,
- onBackendActivity: activity.subscribe,
- conversion: conversionClient,
- authenticator: authClient,
- cancelRequest,
- hints: checked.hints,
- };
- return h(Context.Provider, {
- value,
- children,
- });
-};
-
-/**
- * build http client with cache breaker due to SWR
- * @param url
- * @returns
- */
-function buildApiClient(url: URL) {
- const httpFetch = new BrowserFetchHttpLib({
- enableThrottling: true,
- requireTls: false,
- });
- const httpLib = new ObservableHttpClientLibrary(httpFetch, {
- observe(ev) {
- activity.notify(ev);
- },
- });
-
- function cancelRequest(id: string) {
- httpLib.cancelRequest(id);
- }
-
- const bankClient = new TalerCoreBankHttpClient(
- url.href,
- httpLib,
- evictBankSwrCache,
- );
- const conversionClient = new TalerBankConversionHttpClient(
- bankClient.getConversionInfoAPI().href,
- httpLib,
- evictConversionSwrCache,
- );
- const authClient = (user: string) =>
- new TalerAuthenticationHttpClient(
- bankClient.getAuthenticationAPI(user).href,
- httpLib,
- );
-
- return { bankClient, conversionClient, authClient, cancelRequest };
-}
-
-export const BankCoreApiProviderTesting = ({
- children,
- state,
- url,
-}: {
- children: ComponentChildren;
- state: TalerCorebankApi.Config;
- url: string;
-}): VNode => {
- const value: Type = {
- url: new URL(url),
- config: state,
- // @ts-expect-error this API is not being used, not really needed
- bank: undefined,
- hints: [],
- };
-
- return h(Context.Provider, {
- value,
- children,
- });
-};
-
-const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
- async notifySuccess(op) {
- switch (op) {
- case TalerCoreBankCacheEviction.DELETE_ACCOUNT: {
- await Promise.all([
- revalidatePublicAccounts(),
- revalidateBusinessAccounts(),
- ]);
- return;
- }
- case TalerCoreBankCacheEviction.CREATE_ACCOUNT: {
- // admin balance change on new account
- await Promise.all([
- revalidateAccountDetails(),
- revalidateTransactions(),
- revalidatePublicAccounts(),
- revalidateBusinessAccounts(),
- ]);
- return;
- }
- case TalerCoreBankCacheEviction.UPDATE_ACCOUNT: {
- await Promise.all([revalidateAccountDetails()]);
- return;
- }
- case TalerCoreBankCacheEviction.CREATE_TRANSACTION: {
- await Promise.all([
- revalidateAccountDetails(),
- revalidateTransactions(),
- ]);
- return;
- }
- case TalerCoreBankCacheEviction.CONFIRM_WITHDRAWAL: {
- await Promise.all([
- revalidateAccountDetails(),
- revalidateTransactions(),
- ]);
- return;
- }
- case TalerCoreBankCacheEviction.CREATE_CASHOUT: {
- await Promise.all([
- revalidateAccountDetails(),
- revalidateCashouts(),
- revalidateTransactions(),
- ]);
- return;
- }
- case TalerCoreBankCacheEviction.UPDATE_PASSWORD:
- case TalerCoreBankCacheEviction.ABORT_WITHDRAWAL:
- case TalerCoreBankCacheEviction.CREATE_WITHDRAWAL:
- return;
- default:
- assertUnreachable(op);
- }
- },
-};
-
-const evictConversionSwrCache: CacheEvictor<TalerBankConversionCacheEviction> =
-{
- async notifySuccess(op) {
- switch (op) {
- case TalerBankConversionCacheEviction.UPDATE_RATE: {
- await revalidateConversionInfo();
- return;
- }
- default:
- assertUnreachable(op);
- }
- },
-};
diff --git a/packages/bank-ui/src/context/navigation.ts b/packages/bank-ui/src/context/navigation.ts
deleted file mode 100644
index 9552bf899..000000000
--- a/packages/bank-ui/src/context/navigation.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { ComponentChildren, createContext, h, VNode } from "preact";
-import { useContext, useEffect, useState } from "preact/hooks";
-import { AppLocation } from "../route.js";
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-export type Type = {
- path: string;
- params: Record<string, string>;
- navigateTo: (path: AppLocation) => void;
- // addNavigationListener: (listener: (path: string, params: Record<string, string>) => void) => (() => void);
-};
-
-// @ts-expect-error should not be used without provider
-const Context = createContext<Type>(undefined);
-
-export const useNavigationContext = (): Type => useContext(Context);
-
-function getPathAndParamsFromWindow() {
- const path =
- typeof window !== "undefined" ? window.location.hash.substring(1) : "/";
- const params: Record<string, string> = {};
- if (typeof window !== "undefined") {
- for (const [key, value] of new URLSearchParams(window.location.search)) {
- params[key] = value;
- }
- }
- return { path, params };
-}
-
-const { path: initialPath, params: initialParams } =
- getPathAndParamsFromWindow();
-
-// there is a possibility that if the browser does a redirection
-// (which doesn't go through navigatTo function) and that executed
-// too early (before addEventListener runs) it won't be taking
-// into account
-const PopStateEventType = "popstate";
-
-export const BrowserHashNavigationProvider = ({
- children,
-}: {
- children: ComponentChildren;
-}): VNode => {
- const [{ path, params }, setState] = useState({
- path: initialPath,
- params: initialParams,
- });
- if (typeof window === "undefined") {
- throw Error(
- "Can't use BrowserHashNavigationProvider if there is no window object",
- );
- }
- function navigateTo(path: string) {
- const { params } = getPathAndParamsFromWindow();
- setState({ path, params });
- window.location.href = path;
- }
-
- useEffect(() => {
- function eventListener() {
- setState(getPathAndParamsFromWindow());
- }
- window.addEventListener(PopStateEventType, eventListener);
- return () => {
- window.removeEventListener(PopStateEventType, eventListener);
- };
- }, []);
- return h(Context.Provider, {
- value: { path, params, navigateTo },
- children,
- });
-};
diff --git a/packages/bank-ui/src/context/settings.ts b/packages/bank-ui/src/context/settings.ts
index 053fcbd12..6c61a7b4a 100644
--- a/packages/bank-ui/src/context/settings.ts
+++ b/packages/bank-ui/src/context/settings.ts
@@ -16,16 +16,16 @@
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
-import { BankUiSettings } from "../settings.js";
+import { UiSettings } from "../settings.js";
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-export type Type = BankUiSettings;
+export type Type = UiSettings;
-const initial: BankUiSettings = {};
+const initial: UiSettings = {};
const Context = createContext<Type>(initial);
export const useSettingsContext = (): Type => useContext(Context);
@@ -34,7 +34,7 @@ export const SettingsProvider = ({
children,
value,
}: {
- value: BankUiSettings;
+ value: UiSettings;
children: ComponentChildren;
}): VNode => {
return h(Context.Provider, {
diff --git a/packages/bank-ui/src/hooks/account.ts b/packages/bank-ui/src/hooks/account.ts
index 24309183f..43d43a3f2 100644
--- a/packages/bank-ui/src/hooks/account.ts
+++ b/packages/bank-ui/src/hooks/account.ts
@@ -16,17 +16,18 @@
import {
AccessToken,
+ OperationOk,
TalerCoreBankResultByMethod,
TalerHttpError,
WithdrawalOperationStatus,
} from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
-import { PAGE_SIZE } from "../utils.js";
import { useSessionState } from "./session.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
import _useSWR, { SWRHook, mutate } from "swr";
import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { PAGINATED_LIST_REQUEST } from "../utils.js";
const useSWR = _useSWR as unknown as SWRHook;
export interface InstanceTemplateFilter {
@@ -44,7 +45,9 @@ export function revalidateAccountDetails() {
export function useAccountDetails(account: string) {
const { state: credentials } = useSessionState();
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
async function fetcher([username, token]: [string, AccessToken]) {
return await api.getAccount({ username, token });
@@ -70,7 +73,9 @@ export function revalidateWithdrawalDetails() {
}
export function useWithdrawalDetails(wid: string) {
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
const [latestStatus, setLatestStatus] = useState<WithdrawalOperationStatus>();
async function fetcher([wid, old_state]: [
@@ -123,7 +128,9 @@ export function useTransactionDetails(account: string, tid: number) {
const { state: credentials } = useSessionState();
const token =
credentials.status !== "loggedIn" ? undefined : credentials.token;
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
async function fetcher([username, token, txid]: [
string,
@@ -166,7 +173,9 @@ export function usePublicAccounts(
) {
const [offset, setOffset] = useState<number | undefined>(initial);
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
async function fetcher([account, txid]: [
string | undefined,
@@ -175,7 +184,7 @@ export function usePublicAccounts(
return await api.getPublicAccounts(
{ account },
{
- limit: PAGE_SIZE,
+ limit: PAGINATED_LIST_REQUEST,
offset: txid ? String(txid) : undefined,
order: "asc",
},
@@ -197,36 +206,54 @@ export function usePublicAccounts(
keepPreviousData: true,
});
- const isLastPage =
- data && data.type === "ok" && data.body.public_accounts.length <= PAGE_SIZE;
- const isFirstPage = !offset;
+ if (error) return error;
+ if (data === undefined) return undefined;
+ // if (data.type !== "ok") return data;
+
+ //TODO: row_id should not be optional
+ return buildPaginatedResult(
+ data.body.public_accounts,
+ offset,
+ setOffset,
+ (d) => d.row_id ?? 0,
+ );
+}
- const result =
- data && data.type == "ok" ? structuredClone(data.body.public_accounts) : [];
- if (result.length == PAGE_SIZE + 1) {
+type PaginatedResult<T> = OperationOk<T> & {
+ isLastPage: boolean;
+ isFirstPage: boolean;
+ loadNext(): void;
+ loadFirst(): void;
+};
+//TODO: consider sending this to web-util
+export function buildPaginatedResult<DataType, OffsetId>(
+ data: DataType[],
+ offset: OffsetId | undefined,
+ setOffset: (o: OffsetId | undefined) => void,
+ getId: (r: DataType) => OffsetId,
+): PaginatedResult<DataType[]> {
+ const isLastPage = data.length < PAGINATED_LIST_REQUEST;
+ const isFirstPage = offset === undefined;
+
+ const result = structuredClone(data);
+ if (result.length == PAGINATED_LIST_REQUEST) {
+ //do now show the last element, used to know if this is the last page
result.pop();
}
- const pagination = {
- result,
+ return {
+ type: "ok",
+ body: result,
isLastPage,
isFirstPage,
loadNext: () => {
if (!result.length) return;
- setOffset(result[result.length - 1].row_id);
+ const id = getId(result[result.length - 1]);
+ setOffset(id);
},
loadFirst: () => {
- setOffset(0);
+ setOffset(undefined);
},
};
-
- // const public_accountslist = data?.type !== "ok" ? [] : data.body.public_accounts;
- if (data) {
- return { ok: true, data: data.body, ...pagination };
- }
- if (error) {
- return error;
- }
- return undefined;
}
export function revalidateTransactions() {
@@ -242,7 +269,9 @@ export function useTransactions(account: string, initial?: number) {
credentials.status !== "loggedIn" ? undefined : credentials.token;
const [offset, setOffset] = useState<number | undefined>(initial);
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
async function fetcher([username, token, txid]: [
string,
@@ -252,7 +281,7 @@ export function useTransactions(account: string, initial?: number) {
return await api.getTransactions(
{ username, token },
{
- limit: PAGE_SIZE + 1,
+ limit: PAGINATED_LIST_REQUEST,
offset: txid ? String(txid) : undefined,
order: "dec",
},
@@ -271,34 +300,14 @@ export function useTransactions(account: string, initial?: number) {
revalidateOnFocus: false,
revalidateOnReconnect: false,
});
-
- const isLastPage =
- data && data.type === "ok" && data.body.transactions.length <= PAGE_SIZE;
- const isFirstPage = !offset;
-
- const result =
- data && data.type == "ok" ? structuredClone(data.body.transactions) : [];
- if (result.length == PAGE_SIZE + 1) {
- result.pop();
- }
- const pagination = {
- result,
- isLastPage,
- isFirstPage,
- loadNext: () => {
- if (!result.length) return;
- setOffset(result[result.length - 1].row_id);
- },
- loadFirst: () => {
- setOffset(0);
- },
- };
-
- if (data) {
- return { ok: true, data, ...pagination };
- }
- if (error) {
- return error;
- }
- return undefined;
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return buildPaginatedResult(
+ data.body.transactions,
+ offset,
+ setOffset,
+ (d) => d.row_id,
+ );
}
diff --git a/packages/bank-ui/src/hooks/form.ts b/packages/bank-ui/src/hooks/form.ts
index afa4912eb..fae11c05c 100644
--- a/packages/bank-ui/src/hooks/form.ts
+++ b/packages/bank-ui/src/hooks/form.ts
@@ -72,6 +72,7 @@ function constructFormHandler<T>(
updateForm: (d: FormValues<T>) => void,
errors: FormErrors<T> | undefined,
): FormHandler<T> {
+
const keys = Object.keys(form) as Array<keyof T>;
const handler = keys.reduce((prev, fieldName) => {
@@ -102,6 +103,14 @@ function constructFormHandler<T>(
return handler;
}
+/**
+ * FIXME: Consider sending this to web-utils
+ *
+ *
+ * @param defaultValue
+ * @param check
+ * @returns
+ */
export function useFormState<T>(
defaultValue: FormValues<T>,
check: (f: FormValues<T>) => FormStatus<T>,
diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts
index 274638f74..e0c861a0f 100644
--- a/packages/bank-ui/src/hooks/regional.ts
+++ b/packages/bank-ui/src/hooks/regional.ts
@@ -14,10 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { PAGE_SIZE } from "../utils.js";
import { useSessionState } from "./session.js";
import {
+ AbsoluteTime,
AccessToken,
AmountJson,
Amounts,
@@ -31,19 +31,21 @@ import {
TalerHttpError,
opFixedSuccess,
} from "@gnu-taler/taler-util";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
import _useSWR, { SWRHook, mutate } from "swr";
-import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { buildPaginatedResult } from "./account.js";
+import { PAGINATED_LIST_REQUEST } from "../utils.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
const useSWR = _useSWR as unknown as SWRHook;
export type TransferCalculation =
| {
- debit: AmountJson;
- credit: AmountJson;
- beforeFee: AmountJson;
- }
+ debit: AmountJson;
+ credit: AmountJson;
+ beforeFee: AmountJson;
+ }
| "amount-is-too-small";
type EstimatorFunction = (
amount: AmountJson,
@@ -62,7 +64,10 @@ export function revalidateConversionInfo() {
);
}
export function useConversionInfo() {
- const { lib: { conversion }, config } = useBankCoreApiContext();
+ const {
+ lib: { conversion },
+ config,
+ } = useBankCoreApiContext();
async function fetcher() {
return await conversion.getConfig();
@@ -88,7 +93,9 @@ export function useConversionInfo() {
}
export function useCashinEstimator(): ConversionEstimators {
- const { lib: { conversion } } = useBankCoreApiContext();
+ const {
+ lib: { conversion },
+ } = useBankCoreApiContext();
return {
estimateByCredit: async (fiatAmount, fee) => {
const resp = await conversion.getCashinRate({
@@ -144,7 +151,9 @@ export function useCashinEstimator(): ConversionEstimators {
}
export function useCashoutEstimator(): ConversionEstimators {
- const { lib: { conversion } } = useBankCoreApiContext();
+ const {
+ lib: { conversion },
+ } = useBankCoreApiContext();
return {
estimateByCredit: async (fiatAmount, fee) => {
const resp = await conversion.getCashoutRate({
@@ -217,18 +226,20 @@ export function useBusinessAccounts() {
const { state: credentials } = useSessionState();
const token =
credentials.status !== "loggedIn" ? undefined : credentials.token;
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
const [offset, setOffset] = useState<number | undefined>();
- function fetcher([token, offset]: [AccessToken, number]) {
+ function fetcher([token, aid]: [AccessToken, number]) {
// FIXME: add account name filter
return api.getAccounts(
token,
{},
{
- limit: PAGE_SIZE + 1,
- offset: String(offset),
+ limit: PAGINATED_LIST_REQUEST,
+ offset: aid ? String(aid) : undefined,
order: "asc",
},
);
@@ -249,31 +260,17 @@ export function useBusinessAccounts() {
keepPreviousData: true,
});
- const isLastPage =
- data && data.type === "ok" && data.body.accounts.length <= PAGE_SIZE;
- const isFirstPage = !offset;
-
- const result =
- data && data.type == "ok" ? structuredClone(data.body.accounts) : [];
- if (result.length == PAGE_SIZE + 1) {
- result.pop();
- }
- const pagination = {
- result,
- isLastPage,
- isFirstPage,
- loadNext: () => {
- if (!result.length) return;
- setOffset(result[result.length - 1].row_id);
- },
- loadFirst: () => {
- setOffset(0);
- },
- };
-
- if (data) return { ok: true, data, ...pagination };
if (error) return error;
- return undefined;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ //TODO: row_id should not be optional
+ return buildPaginatedResult(
+ data.body.accounts,
+ offset,
+ setOffset,
+ (d) => d.row_id ?? 0,
+ );
}
type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number };
@@ -290,7 +287,10 @@ export function revalidateOnePendingCashouts() {
}
export function useOnePendingCashouts(account: string) {
const { state: credentials } = useSessionState();
- const { lib: { bank: api }, config } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ config,
+ } = useBankCoreApiContext();
const token =
credentials.status !== "loggedIn" ? undefined : credentials.token;
@@ -350,7 +350,10 @@ export function revalidateCashouts() {
}
export function useCashouts(account: string) {
const { state: credentials } = useSessionState();
- const { lib: { bank: api }, config } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ config,
+ } = useBankCoreApiContext();
const token =
credentials.status !== "loggedIn" ? undefined : credentials.token;
@@ -406,7 +409,9 @@ export function revalidateCashoutDetails() {
export function useCashoutDetails(cashoutId: number | undefined) {
const { state: credentials } = useSessionState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
async function fetcher([username, token, id]: [string, AccessToken, number]) {
return api.getCashoutById({ username, token }, id);
@@ -455,11 +460,13 @@ export function revalidateLastMonitorInfo() {
);
}
export function useLastMonitorInfo(
- currentMoment: number,
- previousMoment: number,
+ currentMoment: AbsoluteTime,
+ previousMoment: AbsoluteTime,
timeframe: TalerCorebankApi.MonitorTimeframeParam,
) {
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
const { state: credentials } = useSessionState();
const token =
credentials.status !== "loggedIn" ? undefined : credentials.token;
@@ -469,8 +476,8 @@ export function useLastMonitorInfo(
TalerCorebankApi.MonitorTimeframeParam,
]) {
const [current, previous] = await Promise.all([
- api.getMonitor(token, { timeframe, which: currentMoment }),
- api.getMonitor(token, { timeframe, which: previousMoment }),
+ api.getMonitor(token, { timeframe, date: currentMoment }),
+ api.getMonitor(token, { timeframe, date: previousMoment }),
]);
return {
current,
diff --git a/packages/bank-ui/src/hooks/session.ts b/packages/bank-ui/src/hooks/session.ts
index 661d64415..4520d0e4a 100644
--- a/packages/bank-ui/src/hooks/session.ts
+++ b/packages/bank-ui/src/hooks/session.ts
@@ -86,7 +86,10 @@ export interface SessionStateHandler {
logIn(info: { username: string; token: AccessToken }): void;
}
-const SESSION_STATE_KEY = buildStorageKey("bank-session", codecForSessionState());
+const SESSION_STATE_KEY = buildStorageKey(
+ "bank-session",
+ codecForSessionState(),
+);
/**
* Return getters and setters for
diff --git a/packages/bank-ui/src/i18n/bank.pot b/packages/bank-ui/src/i18n/bank.pot
index 1f11b8f10..1d8595d17 100644
--- a/packages/bank-ui/src/i18n/bank.pot
+++ b/packages/bank-ui/src/i18n/bank.pot
@@ -263,11 +263,13 @@ msgstr ""
#: src/pages/PaytoWireTransferForm.tsx:457
#, c-format
+msgctxt "wire_transfer"
msgid "Cancel"
msgstr ""
#: src/pages/PaytoWireTransferForm.tsx:471
#, c-format
+msgctxt "wire_transfer"
msgid "Send"
msgstr ""
diff --git a/packages/bank-ui/src/i18n/de.po b/packages/bank-ui/src/i18n/de.po
index ccbbc8208..54fda4377 100644
--- a/packages/bank-ui/src/i18n/de.po
+++ b/packages/bank-ui/src/i18n/de.po
@@ -14,7 +14,7 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2024-03-21 21:39+0000\n"
+"PO-Revision-Date: 2024-05-05 09:32+0000\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"taler-bank-spa/de/>\n"
@@ -23,7 +23,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 5.2.1\n"
+"X-Generator: Weblate 5.4.3\n"
#: src/utils.ts:137
#, c-format
@@ -268,7 +268,7 @@ msgstr ""
#: src/pages/PaytoWireTransferForm.tsx:457
#, c-format
msgid "Cancel"
-msgstr ""
+msgstr "Zurück"
#: src/pages/PaytoWireTransferForm.tsx:471
#, c-format
@@ -490,7 +490,7 @@ msgstr ""
#: src/pages/OperationState/views.tsx:319
#, c-format
msgid "Close"
-msgstr ""
+msgstr "Schließen"
#: src/pages/OperationState/views.tsx:399
#, c-format
@@ -558,7 +558,7 @@ msgstr ""
#: src/pages/WalletWithdrawForm.tsx:253
#, c-format
msgid "Continue"
-msgstr ""
+msgstr "Weiter"
#: src/pages/WalletWithdrawForm.tsx:282
#, c-format
diff --git a/packages/bank-ui/src/i18n/en.po b/packages/bank-ui/src/i18n/en.po
deleted file mode 100644
index a9657bd32..000000000
--- a/packages/bank-ui/src/i18n/en.po
+++ /dev/null
@@ -1,1784 +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/>
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: taler@gnu.org\n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2022-01-08 09:57+0100\n"
-"Last-Translator: <translate@taler.net>\n"
-"Language-Team: English\n"
-"Language: en\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: src/utils.ts:137
-#, c-format
-msgid "Operation failed, please report"
-msgstr ""
-
-#: src/utils.ts:156
-#, c-format
-msgid "Request timeout"
-msgstr ""
-
-#: src/utils.ts:165
-#, c-format
-msgid "Request throttled"
-msgstr ""
-
-#: src/utils.ts:174
-#, c-format
-msgid "Malformed response"
-msgstr ""
-
-#: src/utils.ts:183
-#, c-format
-msgid "Network error"
-msgstr ""
-
-#: src/utils.ts:192
-#, c-format
-msgid "Unexpected request error"
-msgstr ""
-
-#: src/utils.ts:201
-#, c-format
-msgid "Unexpected error"
-msgstr ""
-
-#: src/utils.ts:377
-#, c-format
-msgid "IBAN numbers usually have more that 4 digits"
-msgstr ""
-
-#: src/utils.ts:379
-#, c-format
-msgid "IBAN numbers usually have less that 34 digits"
-msgstr ""
-
-#: src/utils.ts:387
-#, c-format
-msgid "IBAN country code not found"
-msgstr ""
-
-#: src/utils.ts:401
-#, c-format
-msgid "IBAN number is not valid, checksum is wrong"
-msgstr ""
-
-#: src/context/config.ts:136
-#, c-format
-msgid ""
-"the bank backend is not supported. supported version \"%1$s\", server "
-"version \"%2$s\""
-msgstr ""
-
-#: src/hooks/preferences.ts:55
-#, fuzzy, c-format
-msgid "Max withdrawal amount"
-msgstr ""
-
-#: src/hooks/preferences.ts:57
-#, c-format
-msgid "Show withdrawal confirmation"
-msgstr ""
-
-#: src/hooks/preferences.ts:59
-#, c-format
-msgid "Show demo description"
-msgstr ""
-
-#: src/hooks/preferences.ts:61
-#, c-format
-msgid "Show install wallet first"
-msgstr ""
-
-#: src/hooks/preferences.ts:63
-#, fuzzy, c-format
-msgid "Use fast withdrawal form"
-msgstr ""
-
-#: src/hooks/preferences.ts:65
-#, c-format
-msgid "Show debug info"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:90
-#, c-format
-msgid "required"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:92
-#, c-format
-msgid "IBAN should have just uppercased letters and numbers"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:98
-#, c-format
-msgid "not valid"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:100
-#, c-format
-msgid "should be greater than 0"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:102
-#, c-format
-msgid "balance is not enough"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:112
-#, c-format
-msgid "does not follow the pattern"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:114
-#, c-format
-msgid "only \"IBAN\" target are supported"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:116
-#, c-format
-msgid "use the \"amount\" parameter to specify the amount to be transferred"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:118
-#, c-format
-msgid "the amount is not valid"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:120
-#, c-format
-msgid ""
-"use the \"message\" parameter to specify a reference text for the transfer"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:160
-#, c-format
-msgid "The request was invalid or the payto://-URI used unacceptable features."
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:167
-#, c-format
-msgid "Not enough permission to complete the operation."
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:174
-#, c-format
-msgid "The destination account \"%1$s\" was not found."
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:181
-#, c-format
-msgid "The origin and the destination of the transfer can't be the same."
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:188
-#, c-format
-msgid "Your balance is not enough."
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:195
-#, c-format
-msgid "The origin account \"%1$s\" was not found."
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:212
-#, c-format
-msgid "Wire transfer created!"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:270
-#, c-format
-msgid "Using a form"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:310
-#, c-format
-msgid "Import payto:// URI"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:335
-#, c-format
-msgid "Recipient"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:359
-#, c-format
-msgid "IBAN of the recipient's account"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:369
-#, c-format
-msgid "Transfer subject"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:377
-#, c-format
-msgid "subject"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:390
-#, c-format
-msgid "some text to identify the transfer"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:400
-#, c-format
-msgid "Amount"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:415
-#, fuzzy, c-format
-msgid "amount to transfer"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:425
-#, c-format
-msgid "payto URI:"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:436
-#, c-format
-msgid "uniform resource identifier of the target account"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:437
-#, c-format
-msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:457
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: src/pages/PaytoWireTransferForm.tsx:471
-#, c-format
-msgid "Send"
-msgstr ""
-
-#: src/pages/LoginForm.tsx:71
-#, c-format
-msgid "Missing username"
-msgstr ""
-
-#: src/pages/LoginForm.tsx:75
-#, c-format
-msgid "Missing password"
-msgstr ""
-
-#: src/pages/LoginForm.tsx:104
-#, c-format
-msgid "Wrong credentials for \"%1$s\""
-msgstr ""
-
-#: src/pages/LoginForm.tsx:111
-#, c-format
-msgid "Account not found"
-msgstr ""
-
-#: src/pages/LoginForm.tsx:142
-#, c-format
-msgid "Username"
-msgstr ""
-
-#: src/pages/LoginForm.tsx:156
-#, c-format
-msgid "username of the account"
-msgstr ""
-
-#: src/pages/LoginForm.tsx:175
-#, c-format
-msgid "Password"
-msgstr ""
-
-#: src/pages/LoginForm.tsx:188
-#, c-format
-msgid "password of the account"
-msgstr ""
-
-#: src/pages/LoginForm.tsx:223
-#, c-format
-msgid "Check"
-msgstr ""
-
-#: src/pages/LoginForm.tsx:237
-#, c-format
-msgid "Log in"
-msgstr ""
-
-#: src/pages/LoginForm.tsx:249
-#, c-format
-msgid "Register"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:52
-#, c-format
-msgid "Latest transactions"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:63
-#, c-format
-msgid "Date"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:71
-#, c-format
-msgid "Counterpart"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:75
-#, c-format
-msgid "Subject"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:111
-#, c-format
-msgid "sent"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:112
-#, c-format
-msgid "received"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:127
-#, c-format
-msgid "invalid value"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:136
-#, c-format
-msgid "to"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:136
-#, c-format
-msgid "from"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:202
-#, c-format
-msgid "First page"
-msgstr ""
-
-#: src/components/Transactions/views.tsx:209
-#, c-format
-msgid "Next"
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:86
-#, c-format
-msgid "Wire transfer completed!"
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:93
-#, c-format
-msgid "The withdrawal has been aborted previously and can't be confirmed"
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:100
-#, c-format
-msgid ""
-"The withdrawal operation can't be confirmed before a wallet accepted the "
-"transaction."
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:107
-#, c-format
-msgid "The operation id is invalid."
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:114
-#, c-format
-msgid "The operation was not found."
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:121
-#, c-format
-msgid "Your balance is not enough for the operation."
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:155
-#, c-format
-msgid ""
-"The reserve operation has been confirmed previously and can't be aborted"
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:186
-#, fuzzy, c-format
-msgid "Confirm the withdrawal operation"
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:203
-#, c-format
-msgid "Wire transfer details"
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:217
-#, c-format
-msgid "Taler Exchange operator's account"
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:228
-#, c-format
-msgid "Taler Exchange operator's name"
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:317
-#, c-format
-msgid "Transfer"
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:342
-#, c-format
-msgid "Authentication required"
-msgstr ""
-
-#: src/pages/WithdrawalConfirmationQuestion.tsx:352
-#, c-format
-msgid "This operation was created with other username"
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:209
-#, c-format
-msgid ""
-"Unauthorized to make the operation, maybe the session has expired or the "
-"password changed."
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:218
-#, c-format
-msgid "The operation was rejected due to insufficient funds."
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:268
-#, c-format
-msgid "Withdrawal confirmed"
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:272
-#, c-format
-msgid ""
-"The wire transfer to the Taler operator has been initiated. You will soon "
-"receive the requested amount in your Taler wallet."
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:287
-#, c-format
-msgid "Do not show this again"
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:319
-#, c-format
-msgid "Close"
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:399
-#, c-format
-msgid "On this device"
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:404
-#, c-format
-msgid ""
-"If you are using a web browser on desktop you should access your wallet with "
-"the GNU Taler WebExtension now or click the link if your WebExtension have "
-"the \"Inject Taler support\" option enabled."
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:417
-#, c-format
-msgid "Start"
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:426
-#, c-format
-msgid "On a mobile phone"
-msgstr ""
-
-#: src/pages/OperationState/views.tsx:431
-#, c-format
-msgid "Scan the QR code with your mobile device."
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:73
-#, c-format
-msgid "There is an operation already"
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:75
-#, fuzzy, c-format
-msgid "Complete or cancel the operation in"
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:84
-#, c-format
-msgid "this page"
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:101
-#, c-format
-msgid "invalid"
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:116
-#, c-format
-msgid "Server responded with an invalid withdraw URI"
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:117
-#, fuzzy, c-format
-msgid "Withdraw URI: %1$s"
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:132
-#, c-format
-msgid "The operation was rejected due to insufficient funds"
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:253
-#, c-format
-msgid "Continue"
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:282
-#, c-format
-msgid "Prepare your wallet"
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:285
-#, c-format
-msgid ""
-"After using your wallet you will need to confirm or cancel the operation on "
-"this site."
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:295
-#, fuzzy, c-format
-msgid "You need a GNU Taler Wallet"
-msgstr ""
-
-#: src/pages/WalletWithdrawForm.tsx:300
-#, c-format
-msgid "If you don't have one yet you can follow the instruction in"
-msgstr ""
-
-#: src/pages/PaymentOptions.tsx:55
-#, c-format
-msgid "Send money"
-msgstr ""
-
-#: src/pages/PaymentOptions.tsx:73
-#, c-format
-msgid "to a %1$s wallet"
-msgstr ""
-
-#: src/pages/PaymentOptions.tsx:95
-#, c-format
-msgid "Withdraw digital money into your mobile wallet or browser extension"
-msgstr ""
-
-#: src/pages/PaymentOptions.tsx:109
-#, c-format
-msgid "operation ready"
-msgstr ""
-
-#: src/pages/PaymentOptions.tsx:129
-#, c-format
-msgid "to another bank account"
-msgstr ""
-
-#: src/pages/PaymentOptions.tsx:149
-#, c-format
-msgid "Make a wire transfer to an account with known bank account number."
-msgstr ""
-
-#: src/pages/PaymentOptions.tsx:171
-#, fuzzy, c-format
-msgid "Transfer details"
-msgstr ""
-
-#: src/pages/AccountPage/views.tsx:41
-#, c-format
-msgid "This is a demo bank"
-msgstr ""
-
-#: src/pages/AccountPage/views.tsx:46
-#, c-format
-msgid ""
-"This part of the demo shows how a bank that supports Taler directly would "
-"work. In addition to using your own bank account, you can also see the "
-"transaction history of some %1$s."
-msgstr ""
-
-#: src/pages/AccountPage/views.tsx:53
-#, c-format
-msgid ""
-"This part of the demo shows how a bank that supports Taler directly would "
-"work."
-msgstr ""
-
-#: src/pages/AccountPage/views.tsx:70
-#, c-format
-msgid "Pending account delete operation"
-msgstr ""
-
-#: src/pages/AccountPage/views.tsx:72
-#, c-format
-msgid "Pending account update operation"
-msgstr ""
-
-#: src/pages/AccountPage/views.tsx:74
-#, c-format
-msgid "Pending password update operation"
-msgstr ""
-
-#: src/pages/AccountPage/views.tsx:76
-#, c-format
-msgid "Pending transaction operation"
-msgstr ""
-
-#: src/pages/AccountPage/views.tsx:78
-#, c-format
-msgid "Pending withdrawal operation"
-msgstr ""
-
-#: src/pages/AccountPage/views.tsx:80
-#, c-format
-msgid "Pending cashout operation"
-msgstr ""
-
-#: src/pages/AccountPage/views.tsx:91
-#, c-format
-msgid "You can complete or cancel the operation in"
-msgstr ""
-
-#: src/pages/BankFrame.tsx:64
-#, c-format
-msgid "Internal error, please report."
-msgstr ""
-
-#: src/pages/BankFrame.tsx:100
-#, c-format
-msgid "Preferences"
-msgstr ""
-
-#: src/pages/BankFrame.tsx:184
-#, c-format
-msgid "Welcome, %1$s"
-msgstr ""
-
-#: src/pages/WireTransfer.tsx:79
-#, c-format
-msgid "Make a wire transfer"
-msgstr ""
-
-#: src/pages/admin/AccountList.tsx:72
-#, c-format
-msgid "Accounts"
-msgstr ""
-
-#: src/pages/admin/AccountList.tsx:75
-#, c-format
-msgid "A list of all business account in the bank."
-msgstr ""
-
-#: src/pages/admin/AccountList.tsx:86
-#, c-format
-msgid "Create account"
-msgstr ""
-
-#: src/pages/admin/AccountList.tsx:106
-#, c-format
-msgid "Name"
-msgstr ""
-
-#: src/pages/admin/AccountList.tsx:110
-#, c-format
-msgid "Balance"
-msgstr ""
-
-#: src/pages/admin/AccountList.tsx:112
-#, c-format
-msgid "Actions"
-msgstr ""
-
-#: src/pages/admin/AccountList.tsx:151
-#, c-format
-msgid "unknown"
-msgstr ""
-
-#: src/pages/admin/AccountList.tsx:170
-#, c-format
-msgid "change password"
-msgstr ""
-
-#: src/pages/admin/AccountList.tsx:179
-#, c-format
-msgid "cashouts"
-msgstr ""
-
-#: src/pages/admin/AccountList.tsx:189
-#, c-format
-msgid "remove"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:168
-#, c-format
-msgid "Cashout not implemented"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:184
-#, c-format
-msgid "Select a section"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:202
-#, c-format
-msgid "Last hour"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:208
-#, c-format
-msgid "Last day"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:216
-#, c-format
-msgid "Last month"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:222
-#, c-format
-msgid "Last year"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:310
-#, c-format
-msgid "Last Year"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:325
-#, c-format
-msgid "Trading volume on %1$s compared to %2$s"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:342
-#, c-format
-msgid "Cashin"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:352
-#, c-format
-msgid "Cashout"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:364
-#, c-format
-msgid "Payin"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:374
-#, c-format
-msgid "Payout"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:388
-#, c-format
-msgid "download stats as CSV"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:494
-#, c-format
-msgid "Decreased by"
-msgstr ""
-
-#: src/pages/admin/AdminHome.tsx:498
-#, c-format
-msgid "Increased by"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:89
-#, c-format
-msgid "Download bank stats"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:110
-#, c-format
-msgid "Include hour metric"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:143
-#, c-format
-msgid "Include day metric"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:173
-#, c-format
-msgid "Include month metric"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:206
-#, c-format
-msgid "Include year metric"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:239
-#, c-format
-msgid "Include table header"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:272
-#, c-format
-msgid "Add previous metric for compare"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:307
-#, c-format
-msgid "Fail on first error"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:364
-#, c-format
-msgid "Download"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:381
-#, c-format
-msgid "downloading... %1$s"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:399
-#, c-format
-msgid "Download completed"
-msgstr ""
-
-#: src/pages/DownloadStats.tsx:400
-#, c-format
-msgid "click here to save the file in your computer"
-msgstr ""
-
-#: src/pages/PublicHistoriesPage.tsx:78
-#, c-format
-msgid "History of public accounts"
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:48
-#, c-format
-msgid "Currently, the bank is not accepting new registrations!"
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:87
-#, c-format
-msgid "Missing name"
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:91
-#, c-format
-msgid "Use letters and numbers only, and start with a lowercase letter"
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:107
-#, c-format
-msgid "Passwords don't match"
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:130
-#, c-format
-msgid "Server replied with invalid phone or email."
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:137
-#, c-format
-msgid "Registration is disabled because the bank ran out of bonus credit."
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:144
-#, c-format
-msgid "No enough permission to create that account."
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:151
-#, c-format
-msgid "That account id is already taken."
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:158
-#, c-format
-msgid "That username is already taken."
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:165
-#, c-format
-msgid "That username can't be used because is reserved."
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:172
-#, c-format
-msgid "Only admin is allow to set debt limit."
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:179
-#, c-format
-msgid "No information for the selected authentication channel."
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:186
-#, c-format
-msgid "Authentication channel is not supported."
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:193
-#, c-format
-msgid "Only admin can create accounts with second factor authentication."
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:233
-#, c-format
-msgid "Account registration"
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:315
-#, c-format
-msgid "Repeat password"
-msgstr ""
-
-#: src/pages/RegistrationPage.tsx:457
-#, c-format
-msgid "Create a random temporary user"
-msgstr ""
-
-#: src/pages/QrCodeSection.tsx:110
-#, c-format
-msgid "If you have a Taler wallet installed in this device"
-msgstr ""
-
-#: src/pages/QrCodeSection.tsx:116
-#, c-format
-msgid ""
-"You will see the details of the operation in your wallet including the fees "
-"(if applies). If you still don't have one you can install it following "
-"instructions in"
-msgstr ""
-
-#: src/pages/QrCodeSection.tsx:143
-#, fuzzy, c-format
-msgid "Withdraw"
-msgstr ""
-
-#: src/pages/QrCodeSection.tsx:152
-#, c-format
-msgid "Or if you have the wallet in another device"
-msgstr ""
-
-#: src/pages/QrCodeSection.tsx:157
-#, fuzzy, c-format
-msgid "Scan the QR below to start the withdrawal."
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:79
-#, c-format
-msgid "Operation aborted"
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:82
-#, c-format
-msgid ""
-"The wire transfer to the Taler Exchange operator's account was aborted, your "
-"balance was not affected."
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:88
-#, c-format
-msgid "You can close this page now or continue to the account page."
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:147
-#, c-format
-msgid "Done"
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:158
-#, c-format
-msgid "Operation canceled"
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:173
-#, c-format
-msgid ""
-"The operation is marked as 'selected' but some step in the withdrawal failed"
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:175
-#, c-format
-msgid "The account is selected but no withdrawal identification found."
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:188
-#, c-format
-msgid ""
-"There is a withdrawal identification but no account has been selected or the "
-"selected account is invalid."
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:202
-#, c-format
-msgid ""
-"No withdrawal ID found and no account has been selected or the selected "
-"account is invalid."
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:259
-#, c-format
-msgid "Operation not found"
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:263
-#, c-format
-msgid ""
-"This operation is not known by the server. The operation id is wrong or the "
-"server deleted the operation information before reaching here."
-msgstr ""
-
-#: src/pages/WithdrawalQRCode.tsx:278
-#, c-format
-msgid "Cotinue to dashboard"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:98
-#, c-format
-msgid "Cashout not found. It may be also mean that it was already aborted."
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:136
-#, c-format
-msgid "Challenge not found."
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:143
-#, c-format
-msgid "This user is not authorized to complete this challenge."
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:150
-#, c-format
-msgid "Too many attempts, try another code."
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:157
-#, c-format
-msgid "The confirmation code is wrong, try again."
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:164
-#, c-format
-msgid "The operation expired."
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:197
-#, c-format
-msgid "The operation failed."
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:212
-#, c-format
-msgid "The operation needs another confirmation to complete."
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:224
-#, c-format
-msgid "Account delete"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:226
-#, c-format
-msgid "Account update"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:228
-#, c-format
-msgid "Password update"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:230
-#, c-format
-msgid "Wire transfer"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:232
-#, fuzzy, c-format
-msgid "Withdrawal"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:248
-#, fuzzy, c-format
-msgid "Confirm the operation"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:271
-#, c-format
-msgid "Enter the confirmation code"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:313
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:348
-#, c-format
-msgid "Send again"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:359
-#, c-format
-msgid "Send code"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:369
-#, c-format
-msgid "Operation details"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:529
-#, c-format
-msgid "Challenge details"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:536
-#, c-format
-msgid "Sent at"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:551
-#, c-format
-msgid "To phone"
-msgstr ""
-
-#: src/pages/SolveChallengePage.tsx:553
-#, c-format
-msgid "To email"
-msgstr ""
-
-#: src/pages/WithdrawalOperationPage.tsx:49
-#, c-format
-msgid "The Withdrawal URI is not valid"
-msgstr ""
-
-#: src/components/Cashouts/views.tsx:100
-#, c-format
-msgid "Latest cashouts"
-msgstr ""
-
-#: src/components/Cashouts/views.tsx:111
-#, c-format
-msgid "Created"
-msgstr ""
-
-#: src/components/Cashouts/views.tsx:115
-#, c-format
-msgid "Total debit"
-msgstr ""
-
-#: src/components/Cashouts/views.tsx:119
-#, c-format
-msgid "Total credit"
-msgstr ""
-
-#: src/pages/ProfileNavigation.tsx:70
-#, c-format
-msgid "Details"
-msgstr ""
-
-#: src/pages/ProfileNavigation.tsx:74
-#, c-format
-msgid "Delete"
-msgstr ""
-
-#: src/pages/ProfileNavigation.tsx:78
-#, c-format
-msgid "Credentials"
-msgstr ""
-
-#: src/pages/ProfileNavigation.tsx:82
-#, c-format
-msgid "Cashouts"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:95
-#, c-format
-msgid "Unable to create a cashout"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:96
-#, c-format
-msgid "The bank configuration does not support cashout operations."
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:223
-#, c-format
-msgid "need to be higher due to fees"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:225
-#, c-format
-msgid "the total transfer at destination will be zero"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:250
-#, c-format
-msgid "Cashout created"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:272
-#, c-format
-msgid ""
-"Duplicated request detected, check if the operation succeeded or try again."
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:279
-#, c-format
-msgid "The conversion rate was incorrectly applied"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:286
-#, c-format
-msgid "The account does not have sufficient funds"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:293
-#, c-format
-msgid "Cashouts are not supported"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:300
-#, c-format
-msgid "Missing cashout URI in the profile"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:307
-#, c-format
-msgid ""
-"Sending the confirmation message failed, retry later or contact the "
-"administrator."
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:339
-#, c-format
-msgid "Conversion rate"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:360
-#, c-format
-msgid "Fee"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:374
-#, c-format
-msgid "To account"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:381
-#, c-format
-msgid "No cashout account"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:382
-#, c-format
-msgid "Before doing a cashout you need to complete your profile"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:440
-#, fuzzy, c-format
-msgid "Amount to send"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:441
-#, fuzzy, c-format
-msgid "Amount to receive"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:490
-#, c-format
-msgid "Total cost"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:505
-#, c-format
-msgid "Balance left"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:520
-#, c-format
-msgid "Before fee"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:533
-#, c-format
-msgid "Total cashout transfer"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:553
-#, c-format
-msgid "No cashout channel available"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:555
-#, c-format
-msgid ""
-"Before doing a cashout the server need to provide an second channel to "
-"confirm the operation"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:567
-#, c-format
-msgid "Second factor authentication"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:598
-#, c-format
-msgid "Email"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:600
-#, c-format
-msgid "add a email in your profile to enable this option"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:646
-#, c-format
-msgid "SMS"
-msgstr ""
-
-#: src/pages/business/CreateCashout.tsx:648
-#, c-format
-msgid "add a phone number in your profile to enable this option"
-msgstr ""
-
-#: src/pages/account/CashoutListForAccount.tsx:52
-#, c-format
-msgid "Cashout for account %1$s"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:165
-#, c-format
-msgid "it doesn't have the pattern of an IBAN number"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:185
-#, c-format
-msgid "it doesn't have the pattern of an email"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:190
-#, c-format
-msgid "should start with +"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:192
-#, c-format
-msgid "phone number can't have other than numbers"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:329
-#, c-format
-msgid "account identification in the bank"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:365
-#, c-format
-msgid "name of the person owner the account"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:374
-#, c-format
-msgid "Internal IBAN"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:377
-#, c-format
-msgid "if empty a random account number will be assigned"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:378
-#, c-format
-msgid "account identification for bank transfer"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:423
-#, c-format
-msgid "Phone"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:451
-#, c-format
-msgid "Cashout IBAN"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:452
-#, c-format
-msgid "account number where the money is going to be sent when doing cashouts"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:470
-#, c-format
-msgid "Max debt"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:494
-#, c-format
-msgid "how much is user able to transfer after zero balance"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:508
-#, c-format
-msgid "Is this a Taler Exchange?"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:549
-#, c-format
-msgid "This server doesn't support second factor authentication."
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:560
-#, c-format
-msgid "Enable second factor authentication"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:596
-#, c-format
-msgid "Using email"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:654
-#, c-format
-msgid "Using SMS"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:691
-#, c-format
-msgid "Is this account public?"
-msgstr ""
-
-#: src/pages/admin/AccountForm.tsx:719
-#, c-format
-msgid "public accounts have their balance publicly accessible"
-msgstr ""
-
-#: src/pages/account/ShowAccountDetails.tsx:100
-#, c-format
-msgid "Account updated"
-msgstr ""
-
-#: src/pages/account/ShowAccountDetails.tsx:107
-#, c-format
-msgid "The rights to change the account are not sufficient"
-msgstr ""
-
-#: src/pages/account/ShowAccountDetails.tsx:114
-#, c-format
-msgid "The username was not found"
-msgstr ""
-
-#: src/pages/account/ShowAccountDetails.tsx:121
-#, c-format
-msgid ""
-"You can't change the legal name, please contact the your account "
-"administrator."
-msgstr ""
-
-#: src/pages/account/ShowAccountDetails.tsx:128
-#, c-format
-msgid ""
-"You can't change the debt limit, please contact the your account "
-"administrator."
-msgstr ""
-
-#: src/pages/account/ShowAccountDetails.tsx:135
-#, c-format
-msgid ""
-"You can't change the cashout address, please contact the your account "
-"administrator."
-msgstr ""
-
-#: src/pages/account/ShowAccountDetails.tsx:177
-#, c-format
-msgid "Account \"%1$s\""
-msgstr ""
-
-#: src/pages/account/ShowAccountDetails.tsx:190
-#, c-format
-msgid "Change details"
-msgstr ""
-
-#: src/pages/account/ShowAccountDetails.tsx:235
-#, c-format
-msgid "Update"
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:78
-#, c-format
-msgid "password doesn't match"
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:95
-#, c-format
-msgid "Password changed"
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:102
-#, c-format
-msgid "Not authorized to change the password, maybe the session is invalid."
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:112
-#, c-format
-msgid ""
-"You need to provide the old password. If you don't have it contact your "
-"account administrator."
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:117
-#, c-format
-msgid "Your current password doesn't match, can't change to a new password."
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:149
-#, c-format
-msgid "Update password"
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:167
-#, c-format
-msgid "New password"
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:195
-#, c-format
-msgid "Type it again"
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:217
-#, c-format
-msgid "repeat the same password"
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:227
-#, c-format
-msgid "Current password"
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:248
-#, c-format
-msgid "your current password, for security"
-msgstr ""
-
-#: src/pages/account/UpdateAccountPassword.tsx:272
-#, c-format
-msgid "Change"
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:74
-#, c-format
-msgid ""
-"Account created with password \"%1$s\". The user must change the password on "
-"the next login."
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:83
-#, c-format
-msgid "Server replied that phone or email is invalid"
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:90
-#, c-format
-msgid "The rights to perform the operation are not sufficient"
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:97
-#, c-format
-msgid "Account username is already taken"
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:104
-#, c-format
-msgid "Account id is already taken"
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:111
-#, c-format
-msgid "Bank ran out of bonus credit."
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:118
-#, c-format
-msgid "Account username can't be used because is reserved"
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:160
-#, c-format
-msgid "Can't create accounts"
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:161
-#, c-format
-msgid "Only system admin can create accounts."
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:183
-#, c-format
-msgid "New business account"
-msgstr ""
-
-#: src/pages/admin/CreateNewAccount.tsx:209
-#, c-format
-msgid "Create"
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:94
-#, c-format
-msgid "Can't delete the account"
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:95
-#, c-format
-msgid ""
-"The account can't be delete while still holding some balance. First make "
-"sure that the owner make a complete cashout."
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:117
-#, c-format
-msgid "Account removed"
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:124
-#, c-format
-msgid "No enough permission to delete the account."
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:131
-#, c-format
-msgid "The username was not found."
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:138
-#, c-format
-msgid "Can't delete a reserved username."
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:145
-#, c-format
-msgid "Can't delete an account with balance different than zero."
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:170
-#, c-format
-msgid "name doesn't match"
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:180
-#, c-format
-msgid "You are going to remove the account"
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:182
-#, c-format
-msgid "This step can't be undone."
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:188
-#, c-format
-msgid "Deleting account \"%1$s\""
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:206
-#, c-format
-msgid "Verification"
-msgstr ""
-
-#: src/pages/admin/RemoveAccount.tsx:231
-#, c-format
-msgid "enter the account name that is going to be deleted"
-msgstr ""
-
-#: src/pages/business/ShowCashoutDetails.tsx:49
-#, c-format
-msgid "cashout id should be a number"
-msgstr ""
-
-#: src/pages/business/ShowCashoutDetails.tsx:65
-#, c-format
-msgid "This cashout not found. Maybe already aborted."
-msgstr ""
-
-#: src/pages/business/ShowCashoutDetails.tsx:106
-#, c-format
-msgid "Cashout detail"
-msgstr ""
-
-#: src/pages/business/ShowCashoutDetails.tsx:139
-#, c-format
-msgid "Debited"
-msgstr ""
-
-#: src/pages/business/ShowCashoutDetails.tsx:154
-#, c-format
-msgid "Credited"
-msgstr ""
-
-#: src/Routing.tsx:140
-#, c-format
-msgid "Welcome to %1$s!"
-msgstr ""
-
-#, c-format
-#~ msgid "days"
-#~ msgstr ""
-
-#, c-format
-#~ msgid "hours"
-#~ msgstr ""
-
-#, c-format
-#~ msgid "minutes"
-#~ msgstr ""
-
-#, c-format
-#~ msgid "seconds"
-#~ msgstr ""
-
-#~ msgid "Go back"
-#~ msgstr ""
-
-#, fuzzy
-#~ msgid "Withdraw Money into a Taler wallet"
-#~ msgstr ""
-
-#~ msgid "Page has a problem: logged in but backend state is lost."
-#~ msgstr ""
-
-#, fuzzy
-#~ msgid "Welcome to the euFin bank!"
-#~ msgstr ""
-
-#~ msgid "Page has a problem:"
-#~ msgstr ""
-
-#~ msgid "Sign in"
-#~ msgstr ""
diff --git a/packages/bank-ui/src/i18n/es.po b/packages/bank-ui/src/i18n/es.po
index fb69822c5..39527f1dd 100644
--- a/packages/bank-ui/src/i18n/es.po
+++ b/packages/bank-ui/src/i18n/es.po
@@ -271,11 +271,13 @@ msgstr "payto://iban/[iban-destinatario]?message=[asunto]&amount=[%1$s:X.Y]"
#: src/pages/PaytoWireTransferForm.tsx:457
#, c-format
+msgctxt "wire_transfer"
msgid "Cancel"
msgstr "Cancelar"
#: src/pages/PaytoWireTransferForm.tsx:471
#, c-format
+msgctxt "wire_transfer"
msgid "Send"
msgstr "Envíar"
diff --git a/packages/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx
index 6eb7d1b7e..db757ee07 100644
--- a/packages/bank-ui/src/pages/BankFrame.tsx
+++ b/packages/bank-ui/src/pages/BankFrame.tsx
@@ -26,14 +26,16 @@ import {
Footer,
Header,
Loading,
+ RouteDefinition,
ToastBanner,
notifyError,
notifyException,
+ useBankCoreApiContext,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { useEffect, useErrorBoundary, useState } from "preact/hooks";
-import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { privatePages } from "../Routing.js";
import { useSettingsContext } from "../context/settings.js";
import { useAccountDetails } from "../hooks/account.js";
import { useBankState } from "../hooks/bank-state.js";
@@ -43,9 +45,7 @@ import {
usePreferences,
} from "../hooks/preferences.js";
import { useSessionState } from "../hooks/session.js";
-import { RouteDefinition } from "@gnu-taler/web-util/browser";
import { RenderAmount } from "./PaytoWireTransferForm.js";
-import { privatePages } from "../Routing.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -102,9 +102,9 @@ export function BankFrame({
session.state.status !== "loggedIn"
? undefined
: () => {
- session.logOut();
- resetBankState();
- }
+ session.logOut();
+ resetBankState();
+ }
}
sites={
!settings.topNavSites ? [] : Object.entries(settings.topNavSites)
@@ -273,6 +273,7 @@ function AppActivity(): VNode {
case ObservabilityEventType.CryptoStart:
case ObservabilityEventType.CryptoFinishSuccess:
case ObservabilityEventType.CryptoFinishError:
+ case ObservabilityEventType.Message:
return;
default: {
assertUnreachable(ev);
diff --git a/packages/bank-ui/src/pages/LoginForm.tsx b/packages/bank-ui/src/pages/LoginForm.tsx
index 7eed0cd9e..600025400 100644
--- a/packages/bank-ui/src/pages/LoginForm.tsx
+++ b/packages/bank-ui/src/pages/LoginForm.tsx
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { HttpStatusCode, createRFC8959AccessTokenEncoded, createRFC8959AccessTokenPlain } from "@gnu-taler/taler-util";
import {
Button,
LocalNotificationBanner,
@@ -52,7 +52,9 @@ export function LoginForm({
);
const [password, setPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext();
- const { lib: { auth: authenticator } } = useBankCoreApiContext();
+ const {
+ lib: { auth: authenticator },
+ } = useBankCoreApiContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
const { config } = useBankCoreApiContext();
@@ -78,24 +80,24 @@ export function LoginForm({
!username || !password
? undefined
: withErrorHandler(
- async () =>
- authenticator(username).createAccessTokenBasic(username, password, {
- scope: "readwrite",
- duration: { d_us: "forever" },
- refreshable: true,
- }),
- (result) => {
- session.logIn({ username, token: result.body.access_token });
- },
- (fail) => {
- switch (fail.case) {
- case HttpStatusCode.Unauthorized:
- return i18n.str`Wrong credentials for "${username}"`;
- case HttpStatusCode.NotFound:
- return i18n.str`Account not found`;
- }
- },
- );
+ async () =>
+ authenticator(username).createAccessTokenBasic(username, password, {
+ scope: "readwrite",
+ duration: { d_us: "forever" },
+ refreshable: true,
+ }),
+ (result) => {
+ session.logIn({ username, token: createRFC8959AccessTokenEncoded(result.body.access_token) });
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.Unauthorized:
+ return i18n.str`Wrong credentials for "${username}"`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`Account not found`;
+ }
+ },
+ );
return (
<div class="flex min-h-full flex-col justify-center ">
diff --git a/packages/bank-ui/src/pages/OperationState/index.ts b/packages/bank-ui/src/pages/OperationState/index.ts
index 4a7888ee3..38f698a04 100644
--- a/packages/bank-ui/src/pages/OperationState/index.ts
+++ b/packages/bank-ui/src/pages/OperationState/index.ts
@@ -106,15 +106,15 @@ export namespace State {
account: string;
routeHere: RouteDefinition<{ wopid: string }>;
onAbort:
- | undefined
- | (() => Promise<
- TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
- >);
+ | undefined
+ | (() => Promise<
+ TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
+ >);
onConfirm:
- | undefined
- | (() => Promise<
- TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
- >);
+ | undefined
+ | (() => Promise<
+ TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
+ >);
error: undefined;
id: string;
}
diff --git a/packages/bank-ui/src/pages/OperationState/state.ts b/packages/bank-ui/src/pages/OperationState/state.ts
index a0cbc66b9..19c097d18 100644
--- a/packages/bank-ui/src/pages/OperationState/state.ts
+++ b/packages/bank-ui/src/pages/OperationState/state.ts
@@ -45,7 +45,9 @@ export function useComponentState({
const [bankState, updateBankState] = useBankState();
const { state: credentials } = useSessionState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
- const { lib: { bank } } = useBankCoreApiContext();
+ const {
+ lib: { bank },
+ } = useBankCoreApiContext();
const [failure, setFailure] = useState<
TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined
@@ -191,9 +193,9 @@ export function useComponentState({
routeClose,
onAbort: !creds
? async () => {
- onAbort();
- return undefined;
- }
+ onAbort();
+ return undefined;
+ }
: doAbort,
};
}
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
index 22db739b1..3bf891504 100644
--- a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -24,6 +24,7 @@ import {
HttpStatusCode,
PaytoString,
PaytoUri,
+ TalerCorebankApi,
TalerErrorCode,
TranslatedString,
assertUnreachable,
@@ -34,18 +35,19 @@ import {
import {
InternationalizationAPI,
LocalNotificationBanner,
+ RouteDefinition,
ShowInputErrorLabel,
notifyInfo,
+ useBankCoreApiContext,
useLocalNotification,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, Ref, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { mutate } from "swr";
-import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { IdempotencyRetry } from "../../../taler-util/lib/http-client/utils.js";
import { useBankState } from "../hooks/bank-state.js";
import { useSessionState } from "../hooks/session.js";
-import { RouteDefinition } from "@gnu-taler/web-util/browser";
import { undefinedIfEmpty, validateIBAN, validateTalerBank } from "../utils.js";
interface Props {
@@ -82,7 +84,11 @@ export function PaytoWireTransferForm({
const isRawPayto = inputType !== "form";
const { state: credentials } = useSessionState();
- const { lib: { bank: api }, config, url } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ config,
+ url,
+ } = useBankCoreApiContext();
const sendingToFixedAccount = withAccount !== undefined;
@@ -178,12 +184,13 @@ export function PaytoWireTransferForm({
const puri = payto_uri;
const sAmount = sendingAmount;
- await handleError(async () => {
- const request = {
+ await handleError(async function createTransactionHandleError() {
+ const request: TalerCorebankApi.CreateTransactionRequest = {
payto_uri: puri,
amount: sAmount,
};
- const resp = await api.createTransaction(credentials, request);
+ const check = IdempotencyRetry.tryFiveTimes();
+ const resp = await api.createTransaction(credentials, request, check);
mutate(() => true);
if (resp.type === "fail") {
switch (resp.case) {
@@ -214,8 +221,9 @@ export function PaytoWireTransferForm({
case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
return notify({
type: "error",
- title: i18n.str`The destination account "${acName ?? puri
- }" was not found.`,
+ title: i18n.str`The destination account "${
+ acName ?? puri
+ }" was not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
when: AbsoluteTime.now(),
@@ -244,6 +252,15 @@ export function PaytoWireTransferForm({
debug: resp.detail,
when: AbsoluteTime.now(),
});
+ case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: {
+ return notify({
+ type: "error",
+ title: i18n.str`Tried to create the transaction ${check.maxTries} times with different UID but failed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
operation: "create-transaction",
@@ -273,78 +290,6 @@ export function PaytoWireTransferForm({
return (
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 my-4 md:grid-cols-3 bg-gray-100 px-4 pb-4 rounded-lg">
- {/* <div class="">
- <div class="px-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
- <label
- class={
- "relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" +
- (!isRawPayto
- ? "border-indigo-600 ring-2 ring-indigo-600"
- : "border-gray-300")
- }
- >
- <input
- type="radio"
- name="project-type"
- value="Newsletter"
- class="sr-only"
- aria-labelledby="project-type-0-label"
- aria-describedby="project-type-0-description-0 project-type-0-description-1"
- onChange={() => {
- setIsRawPayto(false);
- }}
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span class="block text-sm font-medium text-gray-900">
- <i18n.Translate>Using a form</i18n.Translate>
- </span>
- </span>
- </span>
- </label>
-
- {sendingToFixedAccount ? undefined : (
- <label
- class={
- "relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" +
- (isRawPayto
- ? "border-indigo-600 ring-2 ring-indigo-600"
- : "border-gray-300")
- }
- >
- <input
- type="radio"
- name="project-type"
- value="Existing Customers"
- class="sr-only"
- aria-labelledby="project-type-1-label"
- aria-describedby="project-type-1-description-0 project-type-1-description-1"
- onChange={() => {
-
- setIsRawPayto(true);
- }}
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span class="block text-sm font-medium text-gray-900">
- <i18n.Translate>Import payto:// URI</i18n.Translate>
- </span>
- </span>
- </span>
- </label>
- )}
- {routeCashout ? (
- <a
- name="do cashout"
- href={routeCashout.url({})}
- class="bg-white p-4 rounded-lg text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cashout</i18n.Translate>
- </a>
- ) : undefined}
- </div>
- </div> */}
-
<div>
<fieldset class="px-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
<legend class="sr-only">
@@ -770,13 +715,13 @@ export function InputAmount(
if (
sep_pos !== -1 &&
l - sep_pos - 1 >
- config.currency_specification.num_fractional_input_digits
+ config.currency_specification.num_fractional_input_digits
) {
e.currentTarget.value = e.currentTarget.value.substring(
0,
sep_pos +
- config.currency_specification.num_fractional_input_digits +
- 1,
+ config.currency_specification.num_fractional_input_digits +
+ 1,
);
}
onChange(e.currentTarget.value);
diff --git a/packages/bank-ui/src/pages/ProfileNavigation.tsx b/packages/bank-ui/src/pages/ProfileNavigation.tsx
index 1cf357ceb..3e81e307c 100644
--- a/packages/bank-ui/src/pages/ProfileNavigation.tsx
+++ b/packages/bank-ui/src/pages/ProfileNavigation.tsx
@@ -14,7 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { assertUnreachable } from "@gnu-taler/taler-util";
-import { useNavigationContext, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ useNavigationContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
import { useSessionState } from "../hooks/session.js";
diff --git a/packages/bank-ui/src/pages/PublicHistoriesPage.tsx b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
index 554da0c3f..80ae28dde 100644
--- a/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
@@ -30,10 +30,8 @@ export function PublicHistoriesPage(): VNode {
// TODO: implemented filter by account name
const result = usePublicAccounts(undefined);
const firstAccount =
- result &&
- !(result instanceof TalerError) &&
- result.data.public_accounts.length > 0
- ? result.data.public_accounts[0].username
+ result && !(result instanceof TalerError) && result.body.length > 0
+ ? result.body[0].username
: undefined;
const [showAccount, setShowAccount] = useState(firstAccount);
@@ -45,13 +43,13 @@ export function PublicHistoriesPage(): VNode {
return <Loading />;
}
- const { data } = result;
+ const { body: accountList } = result;
const txs: Record<string, h.JSX.Element> = {};
const accountsBar = [];
// Ask story of all the public accounts.
- for (const account of data.public_accounts) {
+ for (const account of accountList) {
const isSelected = account.username == showAccount;
accountsBar.push(
<li
diff --git a/packages/bank-ui/src/pages/QrCodeSection.tsx b/packages/bank-ui/src/pages/QrCodeSection.tsx
index 156c18f48..359d4c18f 100644
--- a/packages/bank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/bank-ui/src/pages/QrCodeSection.tsx
@@ -51,7 +51,9 @@ export function QrCodeSection({
const [notification, handleError] = useLocalNotificationHandler();
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
const onAbortHandler = handleError(
async () => {
diff --git a/packages/bank-ui/src/pages/RegistrationPage.tsx b/packages/bank-ui/src/pages/RegistrationPage.tsx
index dc08ce0fa..61939c3d6 100644
--- a/packages/bank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/bank-ui/src/pages/RegistrationPage.tsx
@@ -13,22 +13,18 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- AccessToken,
- HttpStatusCode,
- TalerErrorCode,
-} from "@gnu-taler/taler-util";
+import { HttpStatusCode, TalerErrorCode } from "@gnu-taler/taler-util";
import {
LocalNotificationBanner,
+ RouteDefinition,
ShowInputErrorLabel,
+ useBankCoreApiContext,
useLocalNotification,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
import { useSettingsContext } from "../context/settings.js";
-import { RouteDefinition } from "@gnu-taler/web-util/browser";
import { undefinedIfEmpty } from "../utils.js";
import { getRandomPassword, getRandomUsername } from "./rnd.js";
@@ -78,7 +74,9 @@ function RegistrationForm({
const [notification, , handleError] = useLocalNotification();
const settings = useSettingsContext();
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
// const { register } = useTestingAPI();
const { i18n } = useTranslationContext();
@@ -142,6 +140,8 @@ function RegistrationForm({
return i18n.str`Authentication channel is not supported.`;
case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
return i18n.str`Only admin is allow to set debt limit.`;
+ case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT:
+ return i18n.str`Only the administrator can change the minimum cashout limit.`;
case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
return i18n.str`Only admin can create accounts with second factor authentication.`;
}
diff --git a/packages/bank-ui/src/pages/SolveChallengePage.tsx b/packages/bank-ui/src/pages/SolveChallengePage.tsx
index 48d62f1de..624890468 100644
--- a/packages/bank-ui/src/pages/SolveChallengePage.tsx
+++ b/packages/bank-ui/src/pages/SolveChallengePage.tsx
@@ -48,6 +48,7 @@ import { RouteDefinition } from "@gnu-taler/web-util/browser";
import { undefinedIfEmpty } from "../utils.js";
import { RenderAmount } from "./PaytoWireTransferForm.js";
import { OperationNotFound } from "./WithdrawalQRCode.js";
+import { IdempotencyRetry } from "../../../taler-util/lib/http-client/utils.js";
const TAN_PREFIX = "T-";
const TAN_REGEX = /^([Tt](-)?)?[0-9]*$/;
@@ -58,7 +59,9 @@ export function SolveChallengePage({
onChallengeCompleted: () => void;
routeClose: RouteDefinition;
}): VNode {
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
const { i18n } = useTranslationContext();
const [bankState, updateBankState] = useBankState();
const [code, setCode] = useState<string | undefined>(undefined);
@@ -203,7 +206,7 @@ export function SolveChallengePage({
case "update-password":
return await api.updatePassword(creds, ch.request, ch.id);
case "create-transaction":
- return await api.createTransaction(creds, ch.request, ch.id);
+ return await api.createTransaction(creds, ch.request, undefined, ch.id);
case "confirm-withdrawal":
return await api.confirmWithdrawalById(creds, ch.request, ch.id);
case "create-cashout":
diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
index b95b109d5..a9c652643 100644
--- a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
@@ -70,7 +70,10 @@ function OldWithdrawalForm({
// const { navigateTo } = useNavigationContext();
const [bankState, updateBankState] = useBankState();
- const { lib: { bank: api }, config } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ config,
+ } = useBankCoreApiContext();
const { state: credentials } = useSessionState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
@@ -105,12 +108,12 @@ function OldWithdrawalForm({
class="font-semibold text-yellow-700 hover:text-yellow-600"
name="complete operation"
href={url}
- // onClick={(e) => {
- // e.preventDefault()
- // walletInegrationApi.publishTalerAction(uri, () => {
- // navigateTo(url)
- // })
- // }}
+ // onClick={(e) => {
+ // e.preventDefault()
+ // walletInegrationApi.publishTalerAction(uri, () => {
+ // navigateTo(url)
+ // })
+ // }}
>
<i18n.Translate>this page</i18n.Translate>
</a>
@@ -392,7 +395,7 @@ export function WalletWithdrawForm({
routeClose={routeCancel}
routeHere={routeOperationDetails}
onAbort={onOperationAborted}
- // route={routeCancel}
+ // route={routeCancel}
/>
)}
</div>
diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 1eec8bfc2..853dd7bae 100644
--- a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -74,7 +74,10 @@ export function WithdrawalConfirmationQuestion({
const [notification, notify, handleError] = useLocalNotification();
- const { config, lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ config,
+ lib: { bank: api },
+ } = useBankCoreApiContext();
async function doTransfer() {
await handleError(async () => {
@@ -223,20 +226,23 @@ export function WithdrawalConfirmationQuestion({
<dl class="divide-y divide-gray-100">
{((): VNode => {
if (!details.account.isKnown) {
- return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
- <dt class="text-sm font-medium leading-6 text-gray-900">
- <i18n.Translate>
- Payment provider's account
- </i18n.Translate>
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {details.account.targetPath}
- </dd>
- </div>
+ return (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <i18n.Translate>
+ Payment provider's account
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.targetPath}
+ </dd>
+ </div>
+ );
}
switch (details.account.targetType) {
case "iban": {
- const name = details.account.params["receiver-name"];
+ const name =
+ details.account.params["receiver-name"];
return (
<Fragment>
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
@@ -265,13 +271,15 @@ export function WithdrawalConfirmationQuestion({
);
}
case "x-taler-bank": {
- const name = details.account.params["receiver-name"];
+ const name =
+ details.account.params["receiver-name"];
return (
<Fragment>
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">
<i18n.Translate>
- Payment provider's account bank hostname
+ Payment provider's account bank
+ hostname
</i18n.Translate>
</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
@@ -304,7 +312,8 @@ export function WithdrawalConfirmationQuestion({
);
}
case "bitcoin": {
- const name = details.account.params["receiver-name"];
+ const name =
+ details.account.params["receiver-name"];
return (
<Fragment>
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
@@ -333,9 +342,8 @@ export function WithdrawalConfirmationQuestion({
);
}
default: {
- assertUnreachable(details.account)
+ assertUnreachable(details.account);
}
-
}
})()}
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
diff --git a/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
index 9dee1403a..c0c55f14b 100644
--- a/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
@@ -36,7 +36,9 @@ export function WithdrawalOperationPage({
routeClose: RouteDefinition;
routeWithdrawalDetails: RouteDefinition<{ wopid: string }>;
}): VNode {
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
const uri = stringifyWithdrawUri({
bankIntegrationApiBaseUrl: api.getIntegrationAPI().href,
withdrawalOperationId: operationId,
diff --git a/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
index 301978eaa..fd6379895 100644
--- a/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
+++ b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
@@ -25,6 +25,7 @@ interface Props {
account: string;
routeClose: RouteDefinition;
onAuthorizationRequired: () => void;
+ onCashout: () => void;
routeCashoutDetails: RouteDefinition<{ cid: string }>;
routeMyAccountDetails: RouteDefinition;
routeMyAccountDelete: RouteDefinition;
@@ -37,6 +38,7 @@ interface Props {
export function CashoutListForAccount({
account,
onAuthorizationRequired,
+ onCashout,
routeCreateCashout,
routeCashoutDetails,
routeMyAccountCashout,
@@ -76,6 +78,7 @@ export function CashoutListForAccount({
focus
routeHere={routeCreateCashout}
routeClose={routeClose}
+ onCashout={onCashout}
onAuthorizationRequired={onAuthorizationRequired}
account={account}
/>
diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
index bd3961bb7..6db0e5512 100644
--- a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -19,27 +19,26 @@ import {
TalerCorebankApi,
TalerError,
TalerErrorCode,
- TalerRevenueHttpClient,
TranslatedString,
assertUnreachable,
- parsePaytoUri,
+ parsePaytoUri
} from "@gnu-taler/taler-util";
import {
CopyButton,
Loading,
LocalNotificationBanner,
+ RouteDefinition,
notifyInfo,
+ useBankCoreApiContext,
useLocalNotification,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
-import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
import { useAccountDetails } from "../../hooks/account.js";
-import { useSessionState } from "../../hooks/session.js";
import { useBankState } from "../../hooks/bank-state.js";
-import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../../hooks/session.js";
import { LoginForm } from "../LoginForm.js";
import { ProfileNavigation } from "../ProfileNavigation.js";
import { AccountForm } from "../admin/AccountForm.js";
@@ -70,7 +69,9 @@ export function ShowAccountDetails({
const { i18n } = useTranslationContext();
const { state: credentials } = useSessionState();
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
- const { lib: { bank } } = useBankCoreApiContext();
+ const {
+ lib: { bank },
+ } = useBankCoreApiContext();
const accountIsTheCurrentUser =
credentials.status === "loggedIn"
? credentials.username === account
@@ -182,6 +183,15 @@ export function ShowAccountDetails({
when: AbsoluteTime.now(),
});
}
+ case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: {
+ return notify({
+ type: "error",
+ title: i18n.str`Only the administrator can change the minimum cashout limit.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
default:
assertUnreachable(resp);
}
@@ -189,8 +199,7 @@ export function ShowAccountDetails({
});
}
-
- const url = bank.getRevenueAPI(account)
+ const url = bank.getRevenueAPI(account);
url.username = account;
const baseURL = url.href;
@@ -291,15 +300,16 @@ export function ShowAccountDetails({
</h2>
<p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
- Use this information to link your Taler Merchant Backoffice account
- with the current bank account. You can start by copying the values,
- then go to your merchant backoffice service provider, login into
- your account and look for the "import" button in the "bank account" section.
+ Use this information to link your Taler Merchant Backoffice
+ account with the current bank account. You can start by copying
+ the values, then go to your merchant backoffice service provider,
+ login into your account and look for the "import" button in the
+ "bank account" section.
</i18n.Translate>
</p>
</div>
- {payto !== undefined &&
+ {payto !== undefined && (
<div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
@@ -322,82 +332,96 @@ export function ShowAccountDetails({
/>
</div>
<p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Method to use for wire transfer.</i18n.Translate>
+ <i18n.Translate>
+ Method to use for wire transfer.
+ </i18n.Translate>
</p>
</div>
{((payto) => {
switch (payto.targetType) {
case "iban": {
- return <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="iban"
- >
- {i18n.str`IBAN`}
- </label>
- <div class="mt-2">
- <input
- type="text"
- class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="iban"
- id="iban"
- disabled={true}
- value={payto.iban}
- autocomplete="off"
- />
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`IBAN`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={payto.iban}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
</div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>International Bank Account Number.</i18n.Translate>
- </p>
- </div>
+ );
}
case "x-taler-bank": {
- return <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="iban"
- >
- {i18n.str`IBAN`}
- </label>
- <div class="mt-2">
- <input
- type="text"
- class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="iban"
- id="iban"
- disabled={true}
- value={payto.account}
- autocomplete="off"
- />
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`IBAN`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={payto.account}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
</div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>International Bank Account Number.</i18n.Translate>
- </p>
- </div>
+ );
}
case "bitcoin": {
- return <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="iban"
- >
- {i18n.str`Address`}
- </label>
- <div class="mt-2">
- <input
- type="text"
- class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="iban"
- id="iban"
- disabled={true}
- value={"DE1231231231"}
- autocomplete="off"
- />
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`Address`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={true}
+ value={"DE1231231231"}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
</div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>International Bank Account Number.</i18n.Translate>
- </p>
- </div>
+ );
}
}
})(payto)}
@@ -421,7 +445,9 @@ export function ShowAccountDetails({
/>
</div>
<p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Legal name of the person holding the account.</i18n.Translate>
+ <i18n.Translate>
+ Legal name of the person holding the account.
+ </i18n.Translate>
</p>
</div>
<div class="sm:col-span-5">
@@ -443,7 +469,10 @@ export function ShowAccountDetails({
/>
</div>
<p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>From where the merchant can download information about incoming wire transfers to this account.</i18n.Translate>
+ <i18n.Translate>
+ From where the merchant can download information about
+ incoming wire transfers to this account.
+ </i18n.Translate>
</p>
</div>
</div>
@@ -458,15 +487,14 @@ export function ShowAccountDetails({
</a>
<CopyButton
getContent={() => accountLetter ?? ""}
- class="flex text-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
+ class="flex text-center disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
<i18n.Translate>Copy</i18n.Translate>
</CopyButton>
</div>
</div>
- }
-
+ )}
</div>
-
</Fragment>
);
}
diff --git a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
index 58010ecb3..2724fba11 100644
--- a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
+++ b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
@@ -66,7 +66,9 @@ export function UpdateAccountPassword({
const { state: credentials } = useSessionState();
const token =
credentials.status !== "loggedIn" ? undefined : credentials.token;
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
const [current, setCurrent] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx
index 026f6e9b3..ba5da609f 100644
--- a/packages/bank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/bank-ui/src/pages/admin/AccountForm.tsx
@@ -52,6 +52,7 @@ const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
export type AccountFormData = {
debit_threshold?: string;
+ min_cashout?: string;
isExchange?: boolean;
isPublic?: boolean;
name?: string;
@@ -111,6 +112,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
debit_threshold: Amounts.stringifyValue(
template?.debit_threshold ?? config.default_debit_threshold,
),
+ min_cashout: Amounts.stringifyValue(
+ template?.min_cashout ?? `${config.currency}:0`,
+ ),
isExchange: template?.is_taler_exchange,
isPublic: template?.is_public,
name: template?.name ?? "",
@@ -140,12 +144,18 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
(config.allow_edit_cashout_payto_uri || userIsAdmin));
const editableThreshold =
userIsAdmin && (purpose === "create" || purpose === "update");
+ const editableMinCashout =
+ userIsAdmin && (purpose === "create" || purpose === "update");
const editableAccount = purpose === "create" && userIsAdmin;
function updateForm(newForm: typeof defaultValue): void {
- const trimmedAmountStr = newForm.debit_threshold?.trim();
- const parsedAmount = Amounts.parse(
- `${config.currency}:${trimmedAmountStr}`,
+ const trimmedMinCashoutStr = newForm.min_cashout?.trim();
+ const parsedMinCashout = Amounts.parse(
+ `${config.currency}:${trimmedMinCashoutStr}`,
+ );
+ const trimmedDebitThresholdStr = newForm.debit_threshold?.trim();
+ const parsedDebitThreshold = Amounts.parse(
+ `${config.currency}:${trimmedDebitThresholdStr}`,
);
const errors = undefinedIfEmpty<
@@ -189,13 +199,21 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
: undefined,
debit_threshold: !editableThreshold
? undefined
- : !trimmedAmountStr
+ : !trimmedDebitThresholdStr
? undefined
- : !parsedAmount
+ : !parsedDebitThreshold
+ ? i18n.str`Not valid`
+ : undefined,
+ min_cashout: !editableMinCashout
+ ? undefined
+ : !trimmedMinCashoutStr
+ ? undefined
+ : !parsedMinCashout
? i18n.str`Not valid`
: undefined,
name: !editableName
? undefined // disabled
+ : purpose === "update" && newForm.name === undefined ? undefined // the field hasn't been changed
: !newForm.name
? i18n.str`Required`
: undefined,
@@ -248,9 +266,12 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
}
const internalURI = !internal ? undefined : stringifyPaytoUri(internal);
- const threshold = !parsedAmount
+ const threshold = !parsedDebitThreshold
? undefined
- : Amounts.stringify(parsedAmount);
+ : Amounts.stringify(parsedDebitThreshold);
+ const minCashout = !parsedMinCashout
+ ? undefined
+ : Amounts.stringify(parsedMinCashout);
switch (purpose) {
case "create": {
@@ -265,6 +286,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
phone: !newForm.phone ? undefined : newForm.phone,
}),
debit_threshold: threshold ?? config.default_debit_threshold,
+ min_cashout: minCashout,
cashout_payto_uri: cashoutURI,
payto_uri: internalURI,
is_public: newForm.isPublic,
@@ -288,6 +310,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
phone: !newForm.phone ? undefined : newForm.phone,
}),
debit_threshold: threshold,
+ min_cashout: minCashout,
is_public: newForm.isPublic,
name: newForm.name,
tan_channel:
@@ -512,9 +535,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
!editableThreshold
? undefined
: (e) => {
- form.debit_threshold = e as AmountString;
- updateForm(structuredClone(form));
- }
+ form.debit_threshold = e as AmountString;
+ updateForm(structuredClone(form));
+ }
}
/>
<ShowInputErrorLabel
@@ -533,6 +556,38 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
</div>
<div class="sm:col-span-5">
+ <label
+ for="minCashout"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Minimum cashout`}</label>
+ <InputAmount
+ name="minCashout"
+ left
+ currency={config.currency}
+ value={form.min_cashout ?? defaultValue.min_cashout}
+ onChange={
+ !editableMinCashout
+ ? undefined
+ : (e) => {
+ form.min_cashout = e as AmountString;
+ updateForm(structuredClone(form));
+ }
+ }
+ />
+ <ShowInputErrorLabel
+ message={
+ errors?.min_cashout ? String(errors?.min_cashout) : undefined
+ }
+ isDirty={form.min_cashout !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Custom minimum cashout amount for this account.
+ </i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
<span
diff --git a/packages/bank-ui/src/pages/admin/AccountList.tsx b/packages/bank-ui/src/pages/admin/AccountList.tsx
index c4e529f9f..6402c2bcd 100644
--- a/packages/bank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/bank-ui/src/pages/admin/AccountList.tsx
@@ -51,19 +51,19 @@ export function AccountList({
if (result instanceof TalerError) {
return <ErrorLoadingWithDebug error={result} />;
}
- if (result.data.type === "fail") {
- switch (result.data.case) {
+ if (result.type === "fail") {
+ switch (result.case) {
case HttpStatusCode.Unauthorized:
return <Fragment />;
default:
- assertUnreachable(result.data.case);
+ assertUnreachable(result.case);
}
}
const onGoStart = result.isFirstPage ? undefined : result.loadFirst;
const onGoNext = result.isLastPage ? undefined : result.loadNext;
- const accounts = result.result;
+ const accounts = result.body;
return (
<Fragment>
<div class="px-4 sm:px-6 lg:px-8 mt-8">
diff --git a/packages/bank-ui/src/pages/admin/AdminHome.tsx b/packages/bank-ui/src/pages/admin/AdminHome.tsx
index 94b88dc89..34c121235 100644
--- a/packages/bank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/bank-ui/src/pages/admin/AdminHome.tsx
@@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
+ AbsoluteTime,
AmountString,
Amounts,
CurrencySpecification,
@@ -22,26 +23,21 @@ import {
TalerError,
assertUnreachable,
} from "@gnu-taler/taler-util";
-import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ Attention,
+ RouteDefinition,
+ useBankCoreApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import {
format,
- getDate,
- getHours,
- getMonth,
- getYear,
- setDate,
- setHours,
- setMonth,
- setYear,
- sub,
+ sub
} from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import { Transactions } from "../../components/Transactions/index.js";
-import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
import { useConversionInfo, useLastMonitorInfo } from "../../hooks/regional.js";
-import { RouteDefinition } from "@gnu-taler/web-util/browser";
import { RenderAmount } from "../PaytoWireTransferForm.js";
import { WireTransfer } from "../WireTransfer.js";
import { AccountList } from "./AccountList.js";
@@ -95,22 +91,22 @@ export function AdminHome({
}
function getDateForTimeframe(
- which: number,
+ date: AbsoluteTime,
timeframe: TalerCorebankApi.MonitorTimeframeParam,
locale: Locale,
): string {
- const time = Date.now();
+ if (date.t_ms === "never") return "--";
switch (timeframe) {
case TalerCorebankApi.MonitorTimeframeParam.hour:
- return `${format(setHours(time, which), "HH", { locale })}hs`;
+ return `${format(date.t_ms, "HH", { locale })}hs`;
case TalerCorebankApi.MonitorTimeframeParam.day:
- return format(setDate(time, which), "EEEE", { locale });
+ return format(date.t_ms, "EEEE", { locale });
case TalerCorebankApi.MonitorTimeframeParam.month:
- return format(setMonth(time, which), "MMMM", { locale });
+ return format(date.t_ms, "MMMM", { locale });
case TalerCorebankApi.MonitorTimeframeParam.year:
- return format(setYear(time, which), "yyyy", { locale });
+ return format(date.t_ms, "yyyy", { locale });
case TalerCorebankApi.MonitorTimeframeParam.decade:
- return format(setYear(time, which), "yyyy", { locale });
+ return format(date.t_ms, "yyyy", { locale });
}
assertUnreachable(timeframe);
}
@@ -118,32 +114,52 @@ function getDateForTimeframe(
export function getTimeframesForDate(
time: Date,
timeframe: TalerCorebankApi.MonitorTimeframeParam,
-): { current: number; previous: number } {
+): { current: AbsoluteTime; previous: AbsoluteTime } {
switch (timeframe) {
case TalerCorebankApi.MonitorTimeframeParam.hour:
return {
- current: getHours(sub(time, { hours: 1 })),
- previous: getHours(sub(time, { hours: 2 })),
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { hours: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { hours: 2 }).getTime(),
+ ),
};
case TalerCorebankApi.MonitorTimeframeParam.day:
return {
- current: getDate(sub(time, { days: 1 })),
- previous: getDate(sub(time, { days: 2 })),
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { days: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { days: 4 }).getTime(),
+ ),
};
case TalerCorebankApi.MonitorTimeframeParam.month:
return {
- current: getMonth(sub(time, { months: 1 })),
- previous: getMonth(sub(time, { months: 2 })),
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { months: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { months: 2 }).getTime(),
+ ),
};
case TalerCorebankApi.MonitorTimeframeParam.year:
return {
- current: getYear(sub(time, { years: 1 })),
- previous: getYear(sub(time, { years: 2 })),
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 2 }).getTime(),
+ ),
};
case TalerCorebankApi.MonitorTimeframeParam.decade:
return {
- current: getYear(sub(time, { years: 10 })),
- previous: getYear(sub(time, { years: 20 })),
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 10 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 20 }).getTime(),
+ ),
};
default:
assertUnreachable(timeframe);
@@ -185,13 +201,61 @@ function Metrics({
</Attention>
);
}
- default:
+ default: {
assertUnreachable(respInfo.case);
+ }
}
}
- if (resp.current.type !== "ok" || resp.previous.type !== "ok") {
- return <Fragment />;
+ if (resp.current.type !== "ok") {
+ switch (resp.current.case) {
+ case HttpStatusCode.BadRequest:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the current stats failed`}
+ >
+ <i18n.Translate>The request parameters are wrong</i18n.Translate>
+ </Attention>
+ );
+ case HttpStatusCode.Unauthorized:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the current stats failed`}
+ >
+ <i18n.Translate>The user is unauthorized</i18n.Translate>
+ </Attention>
+ );
+ default: {
+ assertUnreachable(resp.current);
+ }
+ }
+ }
+ if (resp.previous.type !== "ok") {
+ switch (resp.previous.case) {
+ case HttpStatusCode.BadRequest:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the previous stats failed`}
+ >
+ <i18n.Translate>The request parameters are wrong</i18n.Translate>
+ </Attention>
+ );
+ case HttpStatusCode.Unauthorized:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the previous stats failed`}
+ >
+ <i18n.Translate>The user is unauthorized</i18n.Translate>
+ </Attention>
+ );
+ default: {
+ assertUnreachable(resp.previous);
+ }
+ }
}
return (
<div class="px-4 mt-4">
@@ -212,10 +276,9 @@ function Metrics({
name="tabs"
class="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
onChange={(e) => {
- // const op = e.currentTarget.value as typeof metricType
setMetricType(
- e.currentTarget
- .value as unknown as TalerCorebankApi.MonitorTimeframeParam,
+ parseInt(e.currentTarget
+ .value, 10) as TalerCorebankApi.MonitorTimeframeParam,
);
}}
>
@@ -363,7 +426,7 @@ function Metrics({
</div>
<dl class="mt-5 grid grid-cols-1 md:grid-cols-2 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow-lg md:divide-x md:divide-y-0">
{resp.current.body.type !== "with-conversions" ||
- resp.previous.body.type !== "with-conversions" ? undefined : (
+ resp.previous.body.type !== "with-conversions" ? undefined : (
<Fragment>
<div class="px-4 py-5 sm:p-6">
<dt class="text-base font-normal text-gray-900">
@@ -462,9 +525,9 @@ function MetricValue({
const rate =
!currAmount ||
- Number.isNaN(currAmount) ||
- !prevAmount ||
- Number.isNaN(prevAmount)
+ Number.isNaN(currAmount) ||
+ !prevAmount ||
+ Number.isNaN(prevAmount)
? 0
: cmp === -1
? 1 - Math.round(currAmount) / Math.round(prevAmount)
diff --git a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
index ecbb18b57..68f39fb9f 100644
--- a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -46,7 +46,9 @@ export function CreateNewAccount({
const { state: credentials } = useSessionState();
const token =
credentials.status !== "loggedIn" ? undefined : credentials.token;
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
const [submitAccount, setSubmitAccount] = useState<
TalerCorebankApi.RegisterAccountRequest | undefined
@@ -144,6 +146,15 @@ export function CreateNewAccount({
debug: resp.detail,
when: AbsoluteTime.now(),
});
+ case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: {
+ return notify({
+ type: "error",
+ title: i18n.str`Only the administrator can change the minimum cashout limit.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
default:
assertUnreachable(resp);
}
diff --git a/packages/bank-ui/src/pages/admin/DownloadStats.tsx b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
index b9ae401e7..8f6bb7c23 100644
--- a/packages/bank-ui/src/pages/admin/DownloadStats.tsx
+++ b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
@@ -59,7 +59,9 @@ export function DownloadStats({ routeCancel }: Props): VNode {
credentials.status !== "loggedIn" || !credentials.isUserAdministrator
? undefined
: credentials;
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
const [options, setOptions] = useState<Options>({
compareWithPrevious: true,
@@ -460,9 +462,9 @@ async function fetchAllStatus(
// await delay()
const previous = options.compareWithPrevious
? await api.getMonitor(token, {
- timeframe: frame.timeframe,
- which: frame.moment.previous,
- })
+ timeframe: frame.timeframe,
+ date: frame.moment.previous,
+ })
: undefined;
if (previous && previous.type === "fail" && options.endOnFirstFail) {
@@ -471,7 +473,7 @@ async function fetchAllStatus(
const current = await api.getMonitor(token, {
timeframe: frame.timeframe,
- which: frame.moment.current,
+ date: frame.moment.current,
});
if (current.type === "fail" && options.endOnFirstFail) {
diff --git a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
index f9c23ea72..dbeebf719 100644
--- a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
@@ -64,7 +64,9 @@ export function RemoveAccount({
const { state } = useSessionState();
const token = state.status !== "loggedIn" ? undefined : state.token;
- const { lib: { bank: api } } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
const [notification, notify, handleError] = useLocalNotification();
const [, updateBankState] = useBankState();
diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
index 7527290d0..485ef5490 100644
--- a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
+++ b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
@@ -104,7 +104,9 @@ function useComponentState({
return function afterComponentLoads() {
const { i18n } = useTranslationContext();
- const { lib: { conversion } } = useBankCoreApiContext();
+ const {
+ lib: { conversion },
+ } = useBankCoreApiContext();
const [notification, notify, handleError] = useLocalNotification();
@@ -519,8 +521,8 @@ function useComponentState({
</div>
{cashoutCalc &&
- status.status === "ok" &&
- Amounts.cmp(status.result.amount, cashoutCalc.credit) <
+ status.status === "ok" &&
+ Amounts.cmp(status.result.amount, cashoutCalc.credit) <
0 ? (
<div class="p-4">
<Attention
diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
index 393240dee..c51b96b8b 100644
--- a/packages/bank-ui/src/pages/regional/CreateCashout.tsx
+++ b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
@@ -59,6 +59,7 @@ interface Props {
account: string;
focus?: boolean;
onAuthorizationRequired: () => void;
+ onCashout: () => void;
routeClose: RouteDefinition;
routeHere: RouteDefinition;
}
@@ -76,6 +77,7 @@ type ErrorFrom<T> = {
export function CreateCashout({
account: accountName,
onAuthorizationRequired,
+ onCashout,
focus,
routeHere,
routeClose,
@@ -90,7 +92,10 @@ export function CreateCashout({
const creds = credentials.status !== "loggedIn" ? undefined : credentials;
const [, updateBankState] = useBankState();
- const { lib: { bank: api }, config, hints } = useBankCoreApiContext();
+ const {
+ lib: { bank: api },
+ config,
+ } = useBankCoreApiContext();
const [form, setForm] = useState<Partial<FormType>>({ isDebit: true });
const [notification, notify, handleError] = useLocalNotification();
const info = useConversionInfo();
@@ -163,13 +168,6 @@ export function CreateCashout({
);
}
- const account = {
- balance: Amounts.parseOrThrow(resultAccount.body.balance.amount),
- balanceIsDebit:
- resultAccount.body.balance.credit_debit_indicator == "debit",
- debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold),
- };
-
const {
fiat_currency,
regional_currency,
@@ -178,6 +176,15 @@ export function CreateCashout({
} = info.body;
const regionalZero = Amounts.zeroOfCurrency(regional_currency);
const fiatZero = Amounts.zeroOfCurrency(fiat_currency);
+
+ const account = {
+ balance: Amounts.parseOrThrow(resultAccount.body.balance.amount),
+ balanceIsDebit:
+ resultAccount.body.balance.credit_debit_indicator == "debit",
+ debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold),
+ minCashout: resultAccount.body.min_cashout === undefined ? regionalZero : Amounts.parseOrThrow(resultAccount.body.min_cashout)
+ };
+
const limit = account.balanceIsDebit
? Amounts.sub(account.debitThreshold, account.balance).amount
: Amounts.add(account.balance, account.debitThreshold).amount;
@@ -196,7 +203,8 @@ export function CreateCashout({
* depending on the isDebit flag
*/
const inputAmount = Amounts.parseOrThrow(
- `${form.isDebit ? regional_currency : fiat_currency}:${!form.amount ? "0" : form.amount
+ `${form.isDebit ? regional_currency : fiat_currency}:${
+ !form.amount ? "0" : form.amount
}`,
);
@@ -236,15 +244,23 @@ export function CreateCashout({
? i18n.str`Invalid`
: Amounts.cmp(limit, calc.debit) === -1
? i18n.str`Balance is not enough`
- : form.isDebit &&
- Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) < 1
- ? i18n.str`Needs to be higher than ${Amounts.stringifyValueWithSpec(
- Amounts.parseOrThrow(conversionInfo.cashout_min_amount),
- regional_currency_specification,
- ).normal
- }`
- : calculationResult === "amount-is-too-small"
+ : calculationResult === "amount-is-too-small"
? i18n.str`Amount needs to be higher`
+ : Amounts.cmp(calc.debit, conversionInfo.cashout_min_amount) < 0
+ ? i18n.str`No account can't cashout less than ${
+ Amounts.stringifyValueWithSpec(
+ Amounts.parseOrThrow(conversionInfo.cashout_min_amount),
+ regional_currency_specification,
+ ).normal
+ }`
+ : Amounts.cmp(calc.debit, account.minCashout) < 0
+ ? i18n.str`Your account can't cashout less than ${
+ Amounts.stringifyValueWithSpec(
+ Amounts.parseOrThrow(account.minCashout),
+ regional_currency_specification,
+ ).normal
+ }`
+
: Amounts.isZero(calc.credit)
? i18n.str`The total transfer at destination will be zero`
: undefined,
@@ -254,21 +270,17 @@ export function CreateCashout({
async function createCashout() {
const request_uid = encodeCrock(getRandomBytes(32));
await handleError(async () => {
- // new cashout api doesn't require channel
- const validChannel = config.supported_tan_channels.length === 0 ||
- form.channel;
-
- if (!creds || !form.subject || !validChannel) return;
+ if (!creds || !form.subject) return;
const request = {
request_uid,
amount_credit: Amounts.stringify(calc.credit),
amount_debit: Amounts.stringify(calc.debit),
subject: form.subject,
- tan_channel: form.channel,
};
const resp = await api.createCashout(creds, request);
if (resp.type === "ok") {
notifyInfo(i18n.str`Cashout created`);
+ onCashout();
} else {
switch (resp.case) {
case HttpStatusCode.Accepted: {
@@ -329,6 +341,15 @@ export function CreateCashout({
debug: resp.detail,
when: AbsoluteTime.now(),
});
+ case TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL:
+ return notify({
+ type: "error",
+ title: i18n.str`The amount is less than the minimum allowed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+
case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
return notify({
type: "error",
@@ -606,9 +627,9 @@ export function CreateCashout({
cashoutDisabled
? undefined
: (value) => {
- form.amount = value;
- updateForm(structuredClone(form));
- }
+ form.amount = value;
+ updateForm(structuredClone(form));
+ }
}
/>
<ShowInputErrorLabel
@@ -649,7 +670,7 @@ export function CreateCashout({
</dd>
</div>
{Amounts.isZero(sellFee) ||
- Amounts.isZero(calc.beforeFee) ? undefined : (
+ Amounts.isZero(calc.beforeFee) ? undefined : (
<div class="flex items-center justify-between border-t-2 afu pt-4">
<dt class="flex items-center text-sm text-gray-600">
<span>
@@ -679,7 +700,6 @@ export function CreateCashout({
</dl>
</div>
)}
-
</div>
</div>
diff --git a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
index eaefeab12..aba00ad7a 100644
--- a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
+++ b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
@@ -138,7 +138,7 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
timestamp={AbsoluteTime.fromProtocolTimestamp(
result.body.creation_time,
)}
- // relative={Duration.fromSpec({ days: 1 })}
+ // relative={Duration.fromSpec({ days: 1 })}
/>
</dd>
</div>
diff --git a/packages/bank-ui/src/route.ts b/packages/bank-ui/src/route.ts
deleted file mode 100644
index 11f13d140..000000000
--- a/packages/bank-ui/src/route.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import { useNavigationContext } from "./context/navigation.js";
-
-declare const __location: unique symbol;
-/**
- * special string that defined a location in the application
- *
- * this help to prevent wrong path
- */
-export type AppLocation = string & {
- [__location]: true;
-};
-export type EmptyObject = Record<string, never>;
-
-export function urlPattern<
- T extends Record<string, string | undefined> = EmptyObject,
->(pattern: RegExp, reverse: (p: T) => string): RouteDefinition<T> {
- const url = reverse as (p: T) => AppLocation;
- return {
- pattern: new RegExp(pattern),
- url,
- };
-}
-
-/**
- * defines a location in the app
- *
- * pattern: how a string will trigger this location
- * url(): how a state serialize to a location
- */
-
-export type ObjectOf<T> = Record<string, T> | EmptyObject;
-
-export type RouteDefinition<
- T extends ObjectOf<string | undefined> = EmptyObject,
-> = {
- pattern: RegExp;
- url: (p: T) => AppLocation;
-};
-
-const nullRountDef = {
- pattern: new RegExp(/.*/),
- url: () => "" as AppLocation,
-};
-export function buildNullRoutDefinition<
- T extends ObjectOf<string>,
->(): RouteDefinition<T> {
- return nullRountDef;
-}
-
-/**
- * Search path in the pageList
- * get the values from the path found
- * add params from searchParams
- *
- * @param path
- * @param params
- */
-function findMatch<T extends ObjectOf<RouteDefinition>>(
- pagesMap: T,
- pageList: Array<keyof T>,
- path: string,
- params: Record<string, string>,
-): Location<T> | undefined {
- for (let idx = 0; idx < pageList.length; idx++) {
- const name = pageList[idx];
- const found = pagesMap[name].pattern.exec(path);
- if (found !== null) {
- const values = {} as Record<string, unknown>;
-
- Object.entries(params).forEach(([key, value]) => {
- values[key] = value;
- });
-
- if (found.groups !== undefined) {
- Object.entries(found.groups).forEach(([key, value]) => {
- values[key] = value;
- });
- }
-
- // @ts-expect-error values is a map string which is equivalent to the RouteParamsType
- return { name, parent: pagesMap, values };
- }
- }
- return undefined;
-}
-
-/**
- * get the type of the params of a location
- *
- */
-type RouteParamsType<
- RouteType,
- Key extends keyof RouteType,
-> = RouteType[Key] extends RouteDefinition<infer ParamType> ? ParamType : never;
-
-/**
- * Helps to create a map of a type with the key
- */
-type MapKeyValue<Type> = {
- [Key in keyof Type]: Key extends string
- ? {
- parent: Type;
- name: Key;
- values: RouteParamsType<Type, Key>;
- }
- : never;
-};
-
-/**
- * create a enumeration of value of a mapped type
- */
-type EnumerationOf<T> = T[keyof T];
-
-type Location<T> = EnumerationOf<MapKeyValue<T>>;
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(
- pagesMap: T,
-): Location<T> | undefined {
- const pageList = Object.keys(pagesMap as object) as Array<keyof T>;
- const { path, params } = useNavigationContext();
-
- return findMatch(pagesMap, pageList, path, params);
-}
diff --git a/packages/bank-ui/src/settings.ts b/packages/bank-ui/src/settings.ts
index 49c8408ce..c085c7cd8 100644
--- a/packages/bank-ui/src/settings.ts
+++ b/packages/bank-ui/src/settings.ts
@@ -24,7 +24,7 @@ import {
codecOptional,
} from "@gnu-taler/taler-util";
-export interface BankUiSettings {
+export interface UiSettings {
// Where libeufin backend is localted
// default: window.origin without "webui/"
backendBaseURL?: string;
@@ -50,7 +50,7 @@ export interface BankUiSettings {
/**
* Global settings for the bank UI.
*/
-const defaultSettings: BankUiSettings = {
+const defaultSettings: UiSettings = {
backendBaseURL: buildDefaultBackendBaseURL(),
iconLinkURL: undefined,
simplePasswordForRandomAccounts: false,
@@ -58,8 +58,8 @@ const defaultSettings: BankUiSettings = {
topNavSites: {},
};
-const codecForBankUISettings = (): Codec<BankUiSettings> =>
- buildCodecForObject<BankUiSettings>()
+const codecForUISettings = (): Codec<UiSettings> =>
+ buildCodecForObject<UiSettings>()
.property("backendBaseURL", codecOptional(codecForString()))
.property("allowRandomAccountCreation", codecOptional(codecForBoolean()))
.property(
@@ -68,7 +68,7 @@ const codecForBankUISettings = (): Codec<BankUiSettings> =>
)
.property("iconLinkURL", codecOptional(codecForString()))
.property("topNavSites", codecOptional(codecForMap(codecForString())))
- .build("BankUiSettings");
+ .build("UiSettings");
function removeUndefineField<T extends object>(obj: T): T {
const keys = Object.keys(obj) as Array<keyof T>;
@@ -80,10 +80,10 @@ function removeUndefineField<T extends object>(obj: T): T {
}, obj);
}
-export function fetchSettings(listener: (s: BankUiSettings) => void): void {
+export function fetchSettings(listener: (s: UiSettings) => void): void {
fetch("./settings.json")
.then((resp) => resp.json())
- .then((json) => codecForBankUISettings().decode(json))
+ .then((json) => codecForUISettings().decode(json))
.then((result) =>
listener({
...defaultSettings,
@@ -104,9 +104,9 @@ function buildDefaultBackendBaseURL(): string | undefined {
).href;
/**
* By default, bank backend serves the html content
- * from the /webui root.
+ * from the /webui root.
*/
return canonicalizeBaseUrl(currentLocation.replace("/webui", ""));
}
- throw Error("No default URL")
+ throw Error("No default URL");
}
diff --git a/packages/bank-ui/src/stories.test.ts b/packages/bank-ui/src/stories.test.ts
index 2f3988e9a..921f9f9ea 100644
--- a/packages/bank-ui/src/stories.test.ts
+++ b/packages/bank-ui/src/stories.test.ts
@@ -23,7 +23,10 @@ import {
TalerCorebankApi,
setupI18n,
} from "@gnu-taler/taler-util";
-import { BankApiProviderTesting, parseGroupImport } from "@gnu-taler/web-util/browser";
+import {
+ BankApiProviderTesting,
+ parseGroupImport,
+} from "@gnu-taler/web-util/browser";
import * as tests from "@gnu-taler/web-util/testing";
import * as components from "./components/index.examples.js";
import * as pages from "./pages/index.stories.js";
diff --git a/packages/bank-ui/src/utils.ts b/packages/bank-ui/src/utils.ts
index 305f13803..2cc502416 100644
--- a/packages/bank-ui/src/utils.ts
+++ b/packages/bank-ui/src/utils.ts
@@ -120,7 +120,11 @@ export enum CashoutStatus {
PENDING = "pending",
}
-export const PAGE_SIZE = 5;
+
+export const PAGINATED_LIST_SIZE = 5;
+// when doing paginated request, ask for one more
+// and use it to know if there are more to request
+export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
type Translator = ReturnType<typeof useTranslationContext>["i18n"];
diff --git a/packages/bank-ui/tailwind.config.js b/packages/bank-ui/tailwind.config.js
index ec51dfbb8..d384690e2 100644
--- a/packages/bank-ui/tailwind.config.js
+++ b/packages/bank-ui/tailwind.config.js
@@ -1,4 +1,18 @@
-/** @type {import('tailwindcss').Config} */
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
export default {
content: {
relative: true,
diff --git a/packages/challenger-ui/build.mjs b/packages/challenger-ui/build.mjs
index 95088628c..166647f79 100755
--- a/packages/challenger-ui/build.mjs
+++ b/packages/challenger-ui/build.mjs
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -20,10 +20,11 @@ import { build } from "@gnu-taler/web-util/build";
await build({
type: "production",
source: {
- js: ["src/main.js"],
+ js: ["src/main.js","src/index.tsx"],
assets: [{
base: "src",
files: [
+ "src/index.html",
"src/attempts-exhausted.html",
"src/enter-address-form.html",
"src/enter-email-form.html",
diff --git a/packages/challenger-ui/copyleft-header.js b/packages/challenger-ui/copyleft-header.js
index 2635717c5..7fa276bea 100644
--- a/packages/challenger-ui/copyleft-header.js
+++ b/packages/challenger-ui/copyleft-header.js
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
diff --git a/packages/challenger-ui/dev.mjs b/packages/challenger-ui/dev.mjs
index 41f6b4210..595c3e99e 100755
--- a/packages/challenger-ui/dev.mjs
+++ b/packages/challenger-ui/dev.mjs
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -18,13 +18,16 @@
import { serve } from "@gnu-taler/web-util/node";
import { initializeDev } from "@gnu-taler/web-util/build";
+const devEntryPoints = ["src/index.tsx", "src/main.js"];
+
const build = initializeDev({
type: "development",
source: {
- js: ["src/main.js"],
+ js: devEntryPoints,
assets: [{
base: "src",
files: [
+ "src/index.html",
"src/attempts-exhausted.html",
"src/enter-address-form.html",
"src/enter-email-form.html",
@@ -39,6 +42,7 @@ const build = initializeDev({
}],
},
destination: "./dist/dev",
+ public: "/app",
css: "postcss",
});
diff --git a/packages/challenger-ui/package.json b/packages/challenger-ui/package.json
index 64201346a..8234e2385 100644
--- a/packages/challenger-ui/package.json
+++ b/packages/challenger-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/challenger-ui",
- "version": "0.1.0",
+ "version": "0.10.7",
"author": "sebasjm",
"license": "AGPL-3.0-OR-LATER",
"description": "UI for GNU Challenger.",
@@ -9,11 +9,12 @@
"scripts": {
"build": "./build.mjs && ./create_must.sh",
"check": "tsc",
- "clean": "rm -rf dist lib",
- "i18n:extract": "pogen extract",
- "i18n:merge": "pogen merge",
- "i18n:emit": "pogen emit",
- "i18n": "pnpm i18n:extract && pnpm i18n:merge && pnpm i18n:emit",
+ "compile": "tsc && ./build.mjs",
+ "test": "./test.mjs && mocha --require source-map-support/register 'dist/test/**/*.test.js' 'dist/test/**/test.js'",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "clean": "rm -rf dist lib tsconfig.tsbuildinfo",
+ "i18n:strings": "pogen extract && pogen merge",
+ "i18n:translations": "pogen emit",
"pretty": "prettier --write src"
},
"eslintConfig": {
@@ -31,18 +32,36 @@
]
},
"devDependencies": {
- "@gnu-taler/pogen": "^0.0.5",
- "@gnu-taler/web-util": "workspace:*",
+ "eslint": "^8.56.0",
+ "@typescript-eslint/eslint-plugin": "^6.19.0",
+ "@typescript-eslint/parser": "^6.19.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.33.2",
+ "@gnu-taler/pogen": "workspace:*",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
+ "@types/chai": "^4.3.0",
+ "@types/history": "^4.7.8",
+ "@types/mocha": "^10.0.1",
+ "@types/node": "^18.11.17",
"autoprefixer": "^10.4.14",
+ "chai": "^4.3.6",
"esbuild": "^0.19.9",
+ "mocha": "9.2.0",
"po2json": "^0.4.5",
- "postcss": "^8.4.23",
- "postcss-cli": "^10.1.0",
- "tailwindcss": "^3.3.2"
+ "tailwindcss": "^3.3.2",
+ "typescript": "5.3.3"
},
"pogen": {
- "domain": "aml-backoffice"
+ "domain": "challenger-ui"
+ },
+ "dependencies": {
+ "swr": "2.0.3",
+ "@gnu-taler/taler-util": "workspace:*",
+ "@gnu-taler/web-util": "workspace:*",
+ "date-fns": "2.29.3",
+ "jed": "1.1.1",
+ "qrcode-generator": "^1.4.4",
+ "preact": "10.11.3"
}
}
diff --git a/packages/challenger-ui/postcss.config.js b/packages/challenger-ui/postcss.config.js
index 2e7af2b7f..c9a60a43c 100644
--- a/packages/challenger-ui/postcss.config.js
+++ b/packages/challenger-ui/postcss.config.js
@@ -1,3 +1,18 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
export default {
plugins: {
tailwindcss: {},
diff --git a/packages/challenger-ui/src/Routing.tsx b/packages/challenger-ui/src/Routing.tsx
new file mode 100644
index 000000000..6166f159a
--- /dev/null
+++ b/packages/challenger-ui/src/Routing.tsx
@@ -0,0 +1,270 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Loading,
+ urlPattern,
+ useCurrentLocation,
+ useNavigationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+import { assertUnreachable } from "@gnu-taler/taler-util";
+import { CheckChallengeIsUpToDate } from "./components/CheckChallengeIsUpToDate.js";
+import { SessionId, useSessionState } from "./hooks/session.js";
+import { AnswerChallenge } from "./pages/AnswerChallenge.js";
+import { AskChallenge } from "./pages/AskChallenge.js";
+import { CallengeCompleted } from "./pages/CallengeCompleted.js";
+import { Frame } from "./pages/Frame.js";
+import { MissingParams } from "./pages/MissingParams.js";
+import { NonceNotFound } from "./pages/NonceNotFound.js";
+import { Setup } from "./pages/Setup.js";
+
+export function Routing(): VNode {
+ // check session and defined if this is
+ // public routing or private
+ return (
+ <Frame>
+ <PublicRounting />
+ </Frame>
+ );
+}
+
+const publicPages = {
+ noinfo: urlPattern<{ nonce: string }>(
+ /\/noinfo\/(?<nonce>[a-zA-Z0-9]+)/,
+ ({ nonce }) => `#/noinfo/${nonce}`,
+ ),
+ authorize: urlPattern<{ nonce: string }>(
+ /\/authorize\/(?<nonce>[a-zA-Z0-9]+)/,
+ ({ nonce }) => `#/authorize/${nonce}`,
+ ),
+ ask: urlPattern<{ nonce: string }>(
+ /\/ask\/(?<nonce>[a-zA-Z0-9]+)/,
+ ({ nonce }) => `#/ask/${nonce}`,
+ ),
+ answer: urlPattern<{ nonce: string }>(
+ /\/answer\/(?<nonce>[a-zA-Z0-9]+)/,
+ ({ nonce }) => `#/answer/${nonce}`,
+ ),
+ completed: urlPattern<{ nonce: string }>(
+ /\/completed\/(?<nonce>[a-zA-Z0-9]+)/,
+ ({ nonce }) => `#/completed/${nonce}`,
+ ),
+ setup: urlPattern<{ client: string }>(
+ /\/setup\/(?<client>[0-9]+)/,
+ ({ client }) => `#/setup/${client}`,
+ ),
+};
+
+function safeGetParam(
+ ps: Record<string, string[]>,
+ n: string,
+): string | undefined {
+ if (!ps[n] || ps[n].length == 0) return undefined;
+ return ps[n][0];
+}
+
+function safeToURL(s: string | undefined): URL | undefined {
+ if (s === undefined) return undefined;
+ try {
+ return new URL(s);
+ } catch (e) {
+ return undefined;
+ }
+}
+
+function PublicRounting(): VNode {
+ const location = useCurrentLocation(publicPages);
+ const { navigateTo } = useNavigationContext();
+ const { start } = useSessionState();
+
+ if (location === undefined) {
+ return <NonceNotFound />;
+ }
+
+ switch (location.name) {
+ case "noinfo": {
+ return <div>no info</div>;
+ }
+ case "setup": {
+ return (
+ <Setup
+ clientId={location.values.client}
+ onCreated={(nonce) => {
+ navigateTo(publicPages.ask.url({ nonce }));
+ //response_type=code
+ //client_id=1
+ //redirect_uri=http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet
+ //state=123
+ }}
+ />
+ );
+ }
+ case "authorize": {
+ const responseType = safeGetParam(location.params, "response_type");
+ const clientId = safeGetParam(location.params, "client_id");
+ const redirectURL = safeToURL(
+ safeGetParam(location.params, "redirect_uri"),
+ );
+ const state = safeGetParam(location.params, "state");
+ // http://localhost:8080/app/#/authorize/ASDASD123?response_type=code&client_id=1&redirect_uri=goog.ecom&state=123
+ //
+
+ // http://localhost:8080/app/?response_type=code&client_id=1&redirect_uri=http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet&state=123#/authorize/X9668AR2CFC26X55H0M87GJZXGM45VD4SZE05C5SNS5FADPWN220
+
+ if (
+ !responseType ||
+ !clientId ||
+ !redirectURL ||
+ !state ||
+ responseType !== "code"
+ ) {
+ return <MissingParams />;
+ }
+ const sessionId: SessionId = {
+ clientId,
+ redirectURL: redirectURL.href,
+ state,
+ };
+ return (
+ <CheckChallengeIsUpToDate
+ sessionId={sessionId}
+ nonce={location.values.nonce}
+ onNoInfo={() => {
+ navigateTo(
+ publicPages.noinfo.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onCompleted={() => {
+ start(sessionId);
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onChangeLeft={() => {
+ start(sessionId);
+ navigateTo(
+ publicPages.ask.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onNoMoreChanges={() => {
+ start(sessionId);
+ navigateTo(
+ publicPages.ask.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ >
+ <Loading />
+ </CheckChallengeIsUpToDate>
+ );
+ }
+ case "ask": {
+ return (
+ <CheckChallengeIsUpToDate
+ nonce={location.values.nonce}
+ onNoInfo={() => {
+ navigateTo(
+ publicPages.noinfo.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onCompleted={() => {
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ >
+ <AskChallenge
+ focus
+ nonce={location.values.nonce}
+ routeSolveChallenge={publicPages.answer}
+ onSendSuccesful={() => {
+ navigateTo(
+ publicPages.answer.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ />
+ </CheckChallengeIsUpToDate>
+ );
+ }
+ case "answer": {
+ return (
+ <CheckChallengeIsUpToDate
+ nonce={location.values.nonce}
+ onNoInfo={() => {
+ navigateTo(
+ publicPages.noinfo.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ onCompleted={() => {
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ >
+ <AnswerChallenge
+ focus
+ nonce={location.values.nonce}
+ routeAsk={publicPages.ask}
+ onComplete={() => {
+ navigateTo(
+ publicPages.completed.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ />
+ </CheckChallengeIsUpToDate>
+ );
+ }
+ case "completed": {
+ return (
+ <CheckChallengeIsUpToDate
+ nonce={location.values.nonce}
+ onNoInfo={() => {
+ navigateTo(
+ publicPages.noinfo.url({
+ nonce: location.values.nonce,
+ }),
+ );
+ }}
+ >
+ <CallengeCompleted nonce={location.values.nonce} />
+ </CheckChallengeIsUpToDate>
+ );
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
diff --git a/packages/challenger-ui/src/app.tsx b/packages/challenger-ui/src/app.tsx
new file mode 100644
index 000000000..2b5c5c815
--- /dev/null
+++ b/packages/challenger-ui/src/app.tsx
@@ -0,0 +1,168 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CacheEvictor,
+ ChallengerCacheEviction,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+ getGlobalLogLevel,
+ setGlobalLogLevelFromString,
+} from "@gnu-taler/taler-util";
+import {
+ BrowserHashNavigationProvider,
+ ChallengerApiProvider,
+ Loading,
+ TalerWalletIntegrationBrowserProvider,
+ TranslationProvider,
+} from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { SWRConfig } from "swr";
+import { Routing } from "./Routing.js";
+// import { BankCoreApiProvider } from "./context/config.js";
+// import { BrowserHashNavigationProvider } from "./context/navigation.js";
+import { SettingsProvider } from "./context/settings.js";
+// import { TalerWalletIntegrationBrowserProvider } from "./context/wallet-integration.js";
+import { VNode, h } from "preact";
+import { strings } from "./i18n/strings.js";
+import { ChallengerUiSettings, fetchSettings } from "./settings.js";
+import { Frame } from "./pages/Frame.js";
+import { revalidateChallengeSession } from "./hooks/challenge.js";
+const WITH_LOCAL_STORAGE_CACHE = false;
+
+const evictBankSwrCache: CacheEvictor<ChallengerCacheEviction> = {
+ async notifySuccess(op) {
+ switch (op) {
+ case ChallengerCacheEviction.CREATE_CHALLENGE: {
+ await Promise.all([revalidateChallengeSession()]);
+ return;
+ }
+ default: {
+ assertUnreachable(op);
+ }
+ }
+ },
+};
+
+export function App(): VNode {
+ const [settings, setSettings] = useState<ChallengerUiSettings>();
+ useEffect(() => {
+ fetchSettings(setSettings);
+ }, []);
+ if (!settings) return <Loading />;
+
+ const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
+ return (
+ <SettingsProvider value={settings}>
+ <TranslationProvider
+ source={strings}
+ forceLang="en"
+ completeness={{
+ es: strings["es"].completeness,
+ de: strings["de"].completeness,
+ }}
+ >
+ <ChallengerApiProvider
+ baseUrl={new URL("/", baseUrl)}
+ frameOnError={Frame}
+ evictors={{
+ challenger: evictBankSwrCache,
+ }}
+ >
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <TalerWalletIntegrationBrowserProvider>
+ <BrowserHashNavigationProvider>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </TalerWalletIntegrationBrowserProvider>
+ </SWRConfig>
+ </ChallengerApiProvider>
+ </TranslationProvider>
+ </SettingsProvider>
+ );
+}
+
+// @ts-expect-error creating a new property for window object
+window.setGlobalLogLevelFromString = setGlobalLogLevelFromString;
+// @ts-expect-error creating a new property for window object
+window.getGlobalLevel = getGlobalLogLevel;
+
+function localStorageProvider(): Map<unknown, unknown> {
+ const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+ window.addEventListener("beforeunload", () => {
+ const appCache = JSON.stringify(Array.from(map.entries()));
+ localStorage.setItem("app-cache", appCache);
+ });
+ return map;
+}
+
+function getInitialBackendBaseURL(
+ backendFromSettings: string | undefined,
+): string {
+ const overrideUrl =
+ typeof localStorage !== "undefined"
+ ? localStorage.getItem("challenger-base-url")
+ : undefined;
+ let result: string;
+
+ if (!overrideUrl) {
+ // normal path
+ if (!backendFromSettings) {
+ console.error(
+ "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
+ );
+ result = window.origin;
+ } else {
+ result = backendFromSettings;
+ }
+ } else {
+ // testing/development path
+ result = overrideUrl;
+ }
+ try {
+ return canonicalizeBaseUrl(result);
+ } catch (e) {
+ // fall back
+ return canonicalizeBaseUrl(window.origin);
+ }
+}
diff --git a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
new file mode 100644
index 000000000..70e41bf1e
--- /dev/null
+++ b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx
@@ -0,0 +1,132 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useChallengeSession } from "../hooks/challenge.js";
+import { SessionId, useSessionState } from "../hooks/session.js";
+
+interface Props {
+ nonce: string;
+ children: ComponentChildren;
+ sessionId?: SessionId;
+ onCompleted?: () => void;
+ onChangeLeft?: () => void;
+ onNoMoreChanges?: () => void;
+ onNoInfo: () => void;
+}
+export function CheckChallengeIsUpToDate({
+ sessionId: sessionFromParam,
+ nonce,
+ children,
+ onCompleted,
+ onChangeLeft,
+ onNoMoreChanges,
+ onNoInfo,
+}: Props): VNode {
+ const { state, updateStatus } = useSessionState();
+ const { i18n } = useTranslationContext();
+
+ const sessionId = sessionFromParam
+ ? sessionFromParam
+ : !state
+ ? undefined
+ : {
+ clientId: state.clientId,
+ redirectURL: state.redirectURL,
+ state: state.state,
+ };
+
+ const result = useChallengeSession(nonce, sessionId);
+ console.log("asd");
+ if (!sessionId) {
+ onNoInfo();
+ return <Loading />;
+ }
+
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
+ }
+
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.BadRequest: {
+ return (
+ <Attention type="danger" title={i18n.str`Bad request`}>
+ <i18n.Translate>
+ Could not start the challenge, check configuration.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ case HttpStatusCode.NotFound: {
+ return (
+ <Attention type="danger" title={i18n.str`Not found`}>
+ <i18n.Translate>Nonce not found</i18n.Translate>
+ </Attention>
+ );
+ }
+ case HttpStatusCode.NotAcceptable: {
+ return (
+ <Attention type="danger" title={i18n.str`Not acceptable`}>
+ <i18n.Translate>
+ Server has wrong template configuration
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ case HttpStatusCode.InternalServerError: {
+ return (
+ <Attention type="danger" title={i18n.str`Internal error`}>
+ <i18n.Translate>Check logs</i18n.Translate>
+ </Attention>
+ );
+ }
+ default:
+ assertUnreachable(result);
+ }
+ }
+
+ updateStatus(result.body);
+
+ if (onCompleted && "redirectURL" in result.body) {
+ onCompleted();
+ return <Loading />;
+ }
+
+ if (onNoMoreChanges && !result.body.changes_left) {
+ onNoMoreChanges();
+ return <Loading />;
+ }
+
+ if (onChangeLeft && !result.body.changes_left) {
+ onChangeLeft();
+ return <Loading />;
+ }
+
+ return <Fragment>{children}</Fragment>;
+}
diff --git a/packages/challenger-ui/src/context/settings.ts b/packages/challenger-ui/src/context/settings.ts
new file mode 100644
index 000000000..679359200
--- /dev/null
+++ b/packages/challenger-ui/src/context/settings.ts
@@ -0,0 +1,44 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { ChallengerUiSettings } from "../settings.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = ChallengerUiSettings;
+
+const initial: ChallengerUiSettings = {};
+const Context = createContext<Type>(initial);
+
+export const useSettingsContext = (): Type => useContext(Context);
+
+export const SettingsProvider = ({
+ children,
+ value,
+}: {
+ value: ChallengerUiSettings;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/challenger-ui/src/hooks/challenge.ts b/packages/challenger-ui/src/hooks/challenge.ts
new file mode 100644
index 000000000..846242816
--- /dev/null
+++ b/packages/challenger-ui/src/hooks/challenge.ts
@@ -0,0 +1,58 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ ChallengerResultByMethod,
+ TalerHttpError,
+} from "@gnu-taler/taler-util";
+import { useChallengerApiContext } from "@gnu-taler/web-util/browser";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { SessionId } from "./session.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export function revalidateChallengeSession() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "login",
+ undefined,
+ { revalidate: true },
+ );
+}
+
+export function useChallengeSession(
+ nonce: string,
+ session: SessionId | undefined,
+) {
+ const {
+ lib: { challenger: api },
+ } = useChallengerApiContext();
+
+ async function fetcher([n, c, r, s]: [string, string, string, string]) {
+ return await api.login(n, c, r, s);
+ }
+ const { data, error } = useSWR<
+ ChallengerResultByMethod<"login">,
+ TalerHttpError
+ >(
+ !session
+ ? undefined
+ : [nonce, session.clientId, session.redirectURL, session.state, "login"],
+ fetcher,
+ {},
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts
new file mode 100644
index 000000000..ed7ea8986
--- /dev/null
+++ b/packages/challenger-ui/src/hooks/session.ts
@@ -0,0 +1,143 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ ChallengerApi,
+ Codec,
+ buildCodecForObject,
+ codecForBoolean,
+ codecForChallengeStatus,
+ codecForNumber,
+ codecForString,
+ codecForStringURL,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { mutate } from "swr";
+
+/**
+ * Has the information to reach and
+ * authenticate at the bank's backend.
+ */
+export type SessionId = {
+ clientId: string;
+ redirectURL: string;
+ state: string;
+};
+
+export type LastChallengeResponse = {
+ attemptsLeft: number;
+ nextSend: string;
+ transmitted: boolean;
+};
+
+export type SessionState = SessionId & {
+ lastTry: LastChallengeResponse | undefined;
+ lastStatus: ChallengerApi.ChallengeStatus | undefined;
+ completedURL: string | undefined;
+};
+export const codecForLastChallengeResponse = (): Codec<LastChallengeResponse> =>
+ buildCodecForObject<LastChallengeResponse>()
+ .property("attemptsLeft", codecForNumber())
+ .property("nextSend", codecForString())
+ .property("transmitted", codecForBoolean())
+ .build("LastChallengeResponse");
+
+export const codecForSessionState = (): Codec<SessionState> =>
+ buildCodecForObject<SessionState>()
+ .property("clientId", codecForString())
+ .property("redirectURL", codecForStringURL())
+ .property("completedURL", codecOptional(codecForStringURL()))
+ .property("state", codecForString())
+ .property("lastStatus", codecOptional(codecForChallengeStatus()))
+ .property("lastTry", codecOptional(codecForLastChallengeResponse()))
+ .build("SessionState");
+
+export interface SessionStateHandler {
+ state: SessionState | undefined;
+ start(s: SessionId): void;
+ accepted(l: LastChallengeResponse): void;
+ completed(e: URL): void;
+ updateStatus(s: ChallengerApi.ChallengeStatus): void;
+}
+
+const SESSION_STATE_KEY = buildStorageKey(
+ "challenger-session",
+ codecForSessionState(),
+);
+
+/**
+ * Return getters and setters for
+ * login credentials and backend's
+ * base URL.
+ */
+export function useSessionState(): SessionStateHandler {
+ const { value: state, update } = useLocalStorage(SESSION_STATE_KEY);
+
+ return {
+ state,
+ start(info) {
+ update({
+ ...info,
+ lastTry: undefined,
+ completedURL: undefined,
+ lastStatus: undefined,
+ });
+ cleanAllCache();
+ },
+ accepted(lastTry) {
+ if (!state) return;
+ update({
+ ...state,
+ lastTry,
+ });
+ },
+ completed(url) {
+ if (!state) return;
+ update({
+ ...state,
+ completedURL: url.href,
+ });
+ },
+ updateStatus(st: ChallengerApi.ChallengeStatus) {
+ if (!state) return;
+ if (!state.lastStatus) {
+ update({
+ ...state,
+ lastStatus: st,
+ });
+ return;
+ }
+ // current status
+ const ls = state.lastStatus;
+ if (
+ ls.changes_left !== st.changes_left ||
+ ls.fix_address !== st.fix_address ||
+ ls.last_address !== st.last_address
+ ) {
+ update({
+ ...state,
+ lastStatus: st,
+ });
+ return;
+ }
+ },
+ };
+}
+
+function cleanAllCache(): void {
+ mutate(() => true, undefined, { revalidate: false });
+}
diff --git a/packages/challenger-ui/src/i18n/challenger-ui.pot b/packages/challenger-ui/src/i18n/challenger-ui.pot
new file mode 100644
index 000000000..5d2497acf
--- /dev/null
+++ b/packages/challenger-ui/src/i18n/challenger-ui.pot
@@ -0,0 +1,199 @@
+# This file is part of GNU Taler
+# (C) 2022-2024 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: src/pages/AnswerChallenge.tsx:55
+#, c-format
+msgid "Can't be empty"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:81
+#, c-format
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:108
+#, c-format
+msgid "Invalid request"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:111
+#, c-format
+msgid "Invalid pin"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:142
+#, c-format
+msgid "Please enter the TAN you received to authenticate."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:147
+#, c-format
+msgid "A TAN was sent to your address &quot;%1$s&quot;."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:151
+#, c-format
+msgid ""
+"We recently already sent a TAN to your address &quot; %1$s&quot;. A new TAN will "
+"not be transmitted again before %2$s."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:161
+#, c-format
+msgid "You can try another PIN but just %1$s times more."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:181
+#, c-format
+msgid "TAN code"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:204
+#, c-format
+msgid "You have %1$s attempts left."
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:217
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/AnswerChallenge.tsx:227
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:76
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:84
+#, c-format
+msgid "invalid email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:86
+#, c-format
+msgid "emails don't match"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:130
+#, c-format
+msgid "Enter contact details"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:133
+#, c-format
+msgid ""
+"You will receive an email with a TAN code that must be provided on the next "
+"page."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:152
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:180
+#, c-format
+msgid "Repeat email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:198
+#, c-format
+msgid "You can change your email address another %1$s times."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:211
+#, c-format
+msgid "Send email"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:237
+#, c-format
+msgid "Bad request"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:238
+#, c-format
+msgid "Could not start the challenge, check configuration."
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:246
+#, c-format
+msgid "Not found"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:247
+#, c-format
+msgid "Nonce not found"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:253
+#, c-format
+msgid "Not acceptable"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:254
+#, c-format
+msgid "Server has wrong template configuration"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:262
+#, c-format
+msgid "Internal error"
+msgstr ""
+
+#: src/pages/AskChallenge.tsx:263
+#, c-format
+msgid "Check logs"
+msgstr ""
+
+#: src/pages/NonceNotFound.tsx:33
+#, c-format
+msgid "The URL is wrong"
+msgstr ""
+
+#: src/pages/NonceNotFound.tsx:36
+#, c-format
+msgid "Maybe the validation check expired."
+msgstr ""
+
+#: src/pages/Setup.tsx:53
+#, c-format
+msgid "Client doesn't exist."
+msgstr ""
+
+#: src/pages/Setup.tsx:65
+#, c-format
+msgid "Setup new challenge with client ID: &quot;%1$s&quot;"
+msgstr ""
+
+#: src/pages/Setup.tsx:76
+#, c-format
+msgid "Start"
+msgstr ""
+
diff --git a/packages/challenger-ui/src/i18n/poheader b/packages/challenger-ui/src/i18n/poheader
new file mode 100644
index 000000000..f2c9d10dd
--- /dev/null
+++ b/packages/challenger-ui/src/i18n/poheader
@@ -0,0 +1,26 @@
+# This file is part of GNU Taler
+# (C) 2022-2024 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Challenger UI\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/challenger-ui/src/i18n/strings.ts b/packages/challenger-ui/src/i18n/strings.ts
new file mode 100644
index 000000000..ea13fed2e
--- /dev/null
+++ b/packages/challenger-ui/src/i18n/strings.ts
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+export interface StringsType {
+ // X-Domain or 'messages'
+ domain: string;
+ lang: string;
+ completeness: number;
+ plural_forms: string;
+ locale_data: {
+ messages: Record<string, unknown>;
+ };
+}
+export const strings: Record<string, StringsType> = {};
+
+strings["it"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ completeness: 14,
+};
+
+strings["es"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ completeness: 100,
+};
+
+strings["en"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "en",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "en",
+ completeness: 100,
+};
+
+strings["de"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ },
+ }
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ completeness: 4,
+};
diff --git a/packages/challenger-ui/src/index.html b/packages/challenger-ui/src/index.html
new file mode 100644
index 000000000..18f472045
--- /dev/null
+++ b/packages/challenger-ui/src/index.html
@@ -0,0 +1,41 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
+<!doctype html>
+<html lang="en" class="h-full bg-gray-100">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri,api" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Challenger</title>
+ <!-- Entry point for the SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+ </head>
+
+ <body class="h-full">
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/packages/challenger-ui/src/index.tsx b/packages/challenger-ui/src/index.tsx
new file mode 100644
index 000000000..f559288a3
--- /dev/null
+++ b/packages/challenger-ui/src/index.tsx
@@ -0,0 +1,27 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { App } from "./app.js";
+import { h, render } from "preact";
+import "./scss/main.css";
+
+const app = document.getElementById("app");
+
+if (app) {
+ render(<App />, app);
+} else {
+ console.error("HTML element with id 'app' not found.");
+}
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
new file mode 100644
index 000000000..73a79c51f
--- /dev/null
+++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx
@@ -0,0 +1,279 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ ChallengerApi,
+ HttpStatusCode,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Button,
+ LocalNotificationBanner,
+ RouteDefinition,
+ ShowInputErrorLabel,
+ useChallengerApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useSessionState } from "../hooks/session.js";
+
+export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
+
+type Props = {
+ nonce: string;
+ focus?: boolean;
+ onComplete: () => void;
+ routeAsk: RouteDefinition<{ nonce: string }>;
+};
+
+export function AnswerChallenge({ focus, nonce, onComplete, routeAsk }: Props): VNode {
+ const { lib } = useChallengerApiContext();
+ const { i18n } = useTranslationContext();
+ const { state, accepted, completed } = useSessionState();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const [pin, setPin] = useState<string | undefined>();
+ const [lastTryError, setLastTryError] =
+ useState<ChallengerApi.InvalidPinResponse>();
+ const errors = undefinedIfEmpty({
+ pin: !pin ? i18n.str`Can't be empty` : undefined,
+ });
+
+ const lastEmail = !state
+ ? undefined
+ : !state.lastStatus
+ ? undefined
+ : !state.lastStatus.last_address
+ ? undefined
+ : state.lastStatus.last_address["email"];
+
+ const onSendAgain =
+ !state || lastEmail === undefined
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ if (!lastEmail) return;
+ return await lib.challenger.challenge(nonce, { email: lastEmail });
+ },
+ (ok) => {
+ if ("redirectURL" in ok.body) {
+ completed(ok.body.redirectURL);
+ } else {
+ accepted({
+ attemptsLeft: ok.body.attempts_left,
+ nextSend: ok.body.next_tx_time,
+ transmitted: ok.body.transmitted,
+ });
+ }
+ return undefined;
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str``;
+ case HttpStatusCode.NotFound:
+ return i18n.str``;
+ case HttpStatusCode.NotAcceptable:
+ return i18n.str``;
+ case HttpStatusCode.TooManyRequests:
+ return i18n.str``;
+ case HttpStatusCode.InternalServerError:
+ return i18n.str``;
+ }
+ },
+ );
+
+ const onCheck =
+ errors !== undefined || (lastTryError && lastTryError.exhausted)
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ return lib.challenger.solve(nonce, { pin: pin! });
+ },
+ (ok) => {
+ completed(ok.body.redirectURL as URL);
+ onComplete();
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str`Invalid request`;
+ case HttpStatusCode.Forbidden: {
+ setLastTryError(fail.body);
+ return i18n.str`Invalid pin`;
+ }
+ case HttpStatusCode.NotFound:
+ return i18n.str``;
+ case HttpStatusCode.NotAcceptable:
+ return i18n.str``;
+ case HttpStatusCode.TooManyRequests:
+ return i18n.str``;
+ case HttpStatusCode.InternalServerError:
+ return i18n.str``;
+ default:
+ assertUnreachable(fail);
+ }
+ },
+ );
+
+ if (!state) {
+ return <div>no state</div>;
+ }
+
+ if (!state.lastTry) {
+ return <div>you should do a challenge first</div>;
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>
+ Enter the TAN you received to authenticate.
+ </i18n.Translate>
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ {state.lastTry.transmitted ? (
+ <i18n.Translate>
+ A TAN was sent to your address &quot;{lastEmail}&quot;.
+ </i18n.Translate>
+ ) : (
+ <Attention title={i18n.str`Resend failed`} type="warning">
+ <i18n.Translate>
+ We recently already sent a TAN to your address &quot;
+ {lastEmail}&quot;. A new TAN will not be transmitted again
+ before &quot;{state.lastTry.nextSend}&quot;.
+ </i18n.Translate>
+ </Attention>
+ )}
+ </p>
+ {!lastTryError ? undefined : (
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <i18n.Translate>
+ You can try another PIN but just{" "}
+ {lastTryError.auth_attempts_left} times more.
+ </i18n.Translate>
+ </p>
+ )}
+ </div>
+ <form
+ method="POST"
+ class="mx-auto mt-16 max-w-xl sm:mt-20"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label
+ for="pin"
+ class="block text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>TAN code</i18n.Translate>
+ </label>
+ <div class="mt-2.5">
+ <input
+ autoFocus
+ ref={focus ? doAutoFocus : undefined}
+ type="number"
+ name="pin"
+ id="pin"
+ maxLength={64}
+ value={pin}
+ onChange={(e) => {
+ setPin(e.currentTarget.value);
+ }}
+ placeholder="12345678"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ />
+ <ShowInputErrorLabel
+ message={errors?.pin}
+ isDirty={pin !== undefined}
+ />
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ <i18n.Translate>
+ You have {state.lastTry.attemptsLeft} attempts left.
+ </i18n.Translate>
+ </p>
+ </div>
+
+ <div class="mt-10">
+ <Button
+ type="submit"
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ disabled={!onCheck}
+ handler={onCheck}
+ >
+ <i18n.Translate>Check</i18n.Translate>
+ </Button>
+ </div>
+ <div class="mt-10 flex justify-between">
+ <div>
+ <a
+ href={routeAsk.url({ nonce })}
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
+ >
+ <i18n.Translate>Change email</i18n.Translate>
+ </a>
+ </div>
+ <div>
+ <Button
+ type="submit"
+ disabled={!onSendAgain}
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onSendAgain}
+ >
+ <i18n.Translate>Send code again</i18n.Translate>
+ </Button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </Fragment>
+ );
+}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null): void {
+ if (element) {
+ setTimeout(() => {
+ element.focus({ preventScroll: true });
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center",
+ });
+ }, 100);
+ }
+}
+
+export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx
new file mode 100644
index 000000000..30b50d707
--- /dev/null
+++ b/packages/challenger-ui/src/pages/AskChallenge.tsx
@@ -0,0 +1,263 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Button,
+ LocalNotificationBanner,
+ RouteDefinition,
+ ShowInputErrorLabel,
+ useChallengerApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useSessionState } from "../hooks/session.js";
+import { doAutoFocus } from "./AnswerChallenge.js";
+
+type Form = {
+ email: string;
+};
+export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
+
+type Props = {
+ nonce: string;
+ onSendSuccesful: () => void;
+ routeSolveChallenge: RouteDefinition<{ nonce: string }>;
+ focus?: boolean;
+};
+
+export function AskChallenge({
+ nonce,
+ onSendSuccesful,
+ routeSolveChallenge,
+ focus,
+}: Props): VNode {
+ const { state, accepted, completed } = useSessionState();
+ const status = state?.lastStatus;
+ const prevEmail =
+ !status || !status.last_address ? undefined : status.last_address["email"];
+ const regexEmail =
+ !status || !status.restrictions ? undefined : status.restrictions["email"];
+
+ const { lib } = useChallengerApiContext();
+ const { i18n } = useTranslationContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const [email, setEmail] = useState<string | undefined>();
+ const [repeat, setRepeat] = useState<string | undefined>();
+
+ const regexTest =
+ regexEmail && regexEmail.regex ? new RegExp(regexEmail.regex) : EMAIL_REGEX;
+ const regexHint =
+ regexEmail && regexEmail.hint ? regexEmail.hint : i18n.str`invalid email`;
+
+ const errors = undefinedIfEmpty({
+ email: !email
+ ? i18n.str`required`
+ : !regexTest.test(email)
+ ? regexHint
+ : prevEmail !== undefined && email === prevEmail
+ ? i18n.str`email should be different`
+ : undefined,
+ repeat: !repeat
+ ? i18n.str`required`
+ : email !== repeat
+ ? i18n.str`emails doesn't match`
+ : undefined,
+ });
+
+ const onSend = errors
+ ? undefined
+ : withErrorHandler(
+ async () => {
+ return lib.challenger.challenge(nonce, { email: email! });
+ },
+ (ok) => {
+ if ("redirectURL" in ok.body) {
+ completed(ok.body.redirectURL);
+ } else {
+ accepted({
+ attemptsLeft: ok.body.attempts_left,
+ nextSend: ok.body.next_tx_time,
+ transmitted: ok.body.transmitted,
+ });
+ }
+ onSendSuccesful();
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str``;
+ case HttpStatusCode.NotFound:
+ return i18n.str``;
+ case HttpStatusCode.NotAcceptable:
+ return i18n.str``;
+ case HttpStatusCode.TooManyRequests:
+ return i18n.str``;
+ case HttpStatusCode.InternalServerError:
+ return i18n.str``;
+ }
+ },
+ );
+
+ if (!status) {
+ return <div>no status loaded</div>;
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>Enter contact details</i18n.Translate>
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <i18n.Translate>
+ You will receive an email with a TAN code that must be provided on
+ the next page.
+ </i18n.Translate>
+ </p>
+ </div>
+ {state.lastTry && (
+ <Fragment>
+ <Attention title={i18n.str`A code has been sent to ${prevEmail}`}>
+ <i18n.Translate>
+ <a href={routeSolveChallenge.url({ nonce })} class="underline">
+ <i18n.Translate>Complete the challenge here.</i18n.Translate>
+ </a>
+ </i18n.Translate>
+ </Attention>
+ </Fragment>
+ )}
+ <form
+ method="POST"
+ class="mx-auto mt-16 max-w-xl sm:mt-20"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="grid grid-cols-1 gap-x-8 gap-y-6">
+ <div class="sm:col-span-2">
+ <label
+ for="email"
+ class="block text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Email</i18n.Translate>
+ </label>
+ <div class="mt-2.5">
+ <input
+ type="email"
+ name="email"
+ id="email"
+ ref={focus ? doAutoFocus : undefined}
+ maxLength={512}
+ autocomplete="email"
+ value={email}
+ onChange={(e) => {
+ setEmail(e.currentTarget.value);
+ }}
+ placeholder={prevEmail}
+ readOnly={status.fix_address}
+ class="block w-full read-only:bg-slate-200 rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ />
+ <ShowInputErrorLabel
+ message={errors?.email}
+ isDirty={email !== undefined}
+ />
+ </div>
+ </div>
+
+ {status.fix_address ? undefined : (
+ <div class="sm:col-span-2">
+ <label
+ for="repeat-email"
+ class="block text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Repeat email</i18n.Translate>
+ </label>
+ <div class="mt-2.5">
+ <input
+ type="email"
+ name="repeat-email"
+ id="repeat-email"
+ value={repeat}
+ onChange={(e) => {
+ setRepeat(e.currentTarget.value);
+ }}
+ autocomplete="email"
+ class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ />
+ <ShowInputErrorLabel
+ message={errors?.repeat}
+ isDirty={repeat !== undefined}
+ />
+ </div>
+ </div>
+ )}
+
+ {!status.changes_left ? (
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ <i18n.Translate>No more changes left</i18n.Translate>
+ </p>
+ ) : (
+ <p class="mt-3 text-sm leading-6 text-gray-400">
+ <i18n.Translate>
+ You can change your email address another{" "}
+ {status.changes_left} times.
+ </i18n.Translate>
+ </p>
+ )}
+ </div>
+
+ {!prevEmail ? (
+ <div class="mt-10">
+ <Button
+ type="submit"
+ disabled={!onSend}
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onSend}
+ >
+ <i18n.Translate>Send email</i18n.Translate>
+ </Button>
+ </div>
+ ) : (
+ <div class="mt-10">
+ <Button
+ type="submit"
+ disabled={!onSend}
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onSend}
+ >
+ <i18n.Translate>Change email</i18n.Translate>
+ </Button>
+ </div>
+ )}
+ </form>
+ </div>
+ </Fragment>
+ );
+}
+
+export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
diff --git a/packages/challenger-ui/src/pages/CallengeCompleted.tsx b/packages/challenger-ui/src/pages/CallengeCompleted.tsx
new file mode 100644
index 000000000..f8cd7ce60
--- /dev/null
+++ b/packages/challenger-ui/src/pages/CallengeCompleted.tsx
@@ -0,0 +1,26 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { VNode, h } from "preact";
+
+type Props = {
+ nonce: string;
+}
+export function CallengeCompleted({nonce}:Props):VNode {
+
+ return <div>
+ completed {nonce}
+ </div>
+} \ No newline at end of file
diff --git a/packages/challenger-ui/src/pages/Frame.tsx b/packages/challenger-ui/src/pages/Frame.tsx
new file mode 100644
index 000000000..612eced0b
--- /dev/null
+++ b/packages/challenger-ui/src/pages/Frame.tsx
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+
+export function Frame({ children }: { children: ComponentChildren }): VNode {
+ return (
+ <Fragment>
+ <header class="bg-indigo-600 w-full mx-auto px-2 border-b border-opacity-25 border-indigo-400">
+ <div class="flex flex-row h-16 items-center ">
+ <div class="flex px-2 justify-start">
+ <div class="flex-shrink-0 bg-white rounded-lg">
+ <a href="#">
+ <img
+ class="h-8 w-auto"
+ src='data:image/svg+xml,<?xml version="1.0" encoding="UTF-8" standalone="no"?>%0A<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">%0A <g fill="%230042b3" fill-rule="evenodd" stroke-width=".3">%0A <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />%0A <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />%0A <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />%0A </g>%0A <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />%0A</svg>'
+ alt="GNU Taler"
+ style="height: 1.5rem; margin: 0.5rem;"
+ />
+ </a>
+ </div>
+ <span class="flex items-center text-white text-lg font-bold ml-4">
+ Challenger
+ </span>
+ </div>
+ <div class="block flex-1 ml-6 "></div>
+ <div class="flex justify-end"></div>
+ </div>
+ </header>
+
+ <main class="flex-1">{children}</main>
+
+ <footer class="bottom-4 mb-4">
+ <div class="mt-8 mx-8 md:order-1 md:mt-0">
+ <div>
+ <p class="text-xs leading-5 text-gray-400">
+ Learn more about{" "}
+ <a
+ target="_blank"
+ rel="noreferrer noopener"
+ class="font-semibold text-gray-500 hover:text-gray-400"
+ href="https://taler.net"
+ >
+ GNU Taler
+ </a>
+ </p>
+ </div>
+ <div style="flex-grow: 1;"></div>
+ <p class="text-xs leading-5 text-gray-400">
+ Copyright © 2014—2023 Taler Systems SA.{" "}
+ </p>
+ </div>
+ </footer>
+ </Fragment>
+ );
+}
diff --git a/packages/aml-backoffice-ui/src/forms.ts b/packages/challenger-ui/src/pages/MissingParams.tsx
index cc9e4c7e8..5eb1e434e 100644
--- a/packages/aml-backoffice-ui/src/forms.ts
+++ b/packages/challenger-ui/src/pages/MissingParams.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -13,12 +13,10 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { VNode, h } from "preact";
-export * from "./forms/index.js";
-
-/**
- * this file is here to have a flat dist folder
- *
- * this file is being build in a bundle separated
- * from the main one.
- */
+export function MissingParams() :VNode {
+ return <div>
+ missing params: {window.location.href}
+ </div>
+} \ No newline at end of file
diff --git a/packages/challenger-ui/src/pages/NonceNotFound.tsx b/packages/challenger-ui/src/pages/NonceNotFound.tsx
new file mode 100644
index 000000000..16b3d90ef
--- /dev/null
+++ b/packages/challenger-ui/src/pages/NonceNotFound.tsx
@@ -0,0 +1,42 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+type Form = {
+ email: string;
+};
+
+export function NonceNotFound(): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>The URL is wrong</i18n.Translate>
+ </h2>
+ <p class="mt-2 text-lg leading-8 text-gray-600">
+ <i18n.Translate>Maybe the validation check expired.</i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/challenger-ui/src/pages/Setup.tsx b/packages/challenger-ui/src/pages/Setup.tsx
new file mode 100644
index 000000000..f431835aa
--- /dev/null
+++ b/packages/challenger-ui/src/pages/Setup.tsx
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { AccessToken, HttpStatusCode, encodeCrock, randomBytes } from "@gnu-taler/taler-util";
+import {
+ Button,
+ LocalNotificationBanner,
+ useChallengerApiContext,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useSessionState } from "../hooks/session.js";
+
+type Props = {
+ clientId: string;
+ onCreated: (nonce:string) => void;
+};
+export function Setup({ clientId, onCreated }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const { lib } = useChallengerApiContext();
+ const { start } = useSessionState();
+
+ const onStart = withErrorHandler(
+ async () => {
+ return lib.challenger.setup(clientId, "secret-token:chal-secret" as AccessToken);
+ },
+ (ok) => {
+ start({
+ clientId,
+ redirectURL: "http://exchange.taler.test:1180/kyc-proof/kyc-provider-wallet",
+ state: encodeCrock(randomBytes(32)),
+ });
+
+ onCreated(ok.body.nonce);
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.NotFound:
+ return i18n.str`Client doesn't exist.`;
+ }
+ },
+ );
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+
+ <div class="isolate bg-white px-6 py-12">
+ <div class="mx-auto max-w-2xl text-center">
+ <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
+ <i18n.Translate>
+ Setup new challenge with client ID: &quot;{clientId}&quot;
+ </i18n.Translate>
+ </h2>
+ </div>
+ <div class="mt-10">
+ <Button
+ type="submit"
+ class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ handler={onStart}
+ >
+ <i18n.Translate>Start</i18n.Translate>
+ </Button>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/challenger-ui/src/settings.json b/packages/challenger-ui/src/settings.json
new file mode 100644
index 000000000..b3d0476aa
--- /dev/null
+++ b/packages/challenger-ui/src/settings.json
@@ -0,0 +1,3 @@
+{
+ "backendBaseURL": "http://challenger.taler.test:1180/"
+}
diff --git a/packages/challenger-ui/src/settings.ts b/packages/challenger-ui/src/settings.ts
new file mode 100644
index 000000000..61d2117fa
--- /dev/null
+++ b/packages/challenger-ui/src/settings.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Codec,
+ buildCodecForObject,
+ canonicalizeBaseUrl,
+ codecForString,
+ codecOptional
+} from "@gnu-taler/taler-util";
+
+export interface ChallengerUiSettings {
+ // Where challenger backend is localted
+ // default: window.origin without "webui/"
+ backendBaseURL?: string;
+
+}
+
+/**
+ * Global settings for the bank UI.
+ */
+const defaultSettings: ChallengerUiSettings = {
+ backendBaseURL: buildDefaultBackendBaseURL(),
+};
+
+const codecForChallengerUISettings = (): Codec<ChallengerUiSettings> =>
+ buildCodecForObject<ChallengerUiSettings>()
+ .property("backendBaseURL", codecOptional(codecForString()))
+ .build("codecForChallengerUISettings");
+
+function removeUndefineField<T extends object>(obj: T): T {
+ const keys = Object.keys(obj) as Array<keyof T>;
+ return keys.reduce((prev, cur) => {
+ if (typeof prev[cur] === "undefined") {
+ delete prev[cur];
+ }
+ return prev;
+ }, obj);
+}
+
+export function fetchSettings(listener: (s: ChallengerUiSettings) => void): void {
+ fetch("./settings.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForChallengerUISettings().decode(json))
+ .then((result) =>
+ listener({
+ ...defaultSettings,
+ ...removeUndefineField(result),
+ }),
+ )
+ .catch((e) => {
+ console.log("failed to fetch settings", e);
+ listener(defaultSettings);
+ });
+}
+
+function buildDefaultBackendBaseURL(): string | undefined {
+ if (typeof window !== "undefined") {
+ const currentLocation = new URL(
+ window.location.pathname,
+ window.location.origin,
+ ).href;
+ /**
+ * By default, bank backend serves the html content
+ * from the /webui root.
+ */
+ return canonicalizeBaseUrl(currentLocation.replace("/webui", ""));
+ }
+ throw Error("No default URL");
+}
diff --git a/packages/challenger-ui/tailwind.config.js b/packages/challenger-ui/tailwind.config.js
index ec51dfbb8..d384690e2 100644
--- a/packages/challenger-ui/tailwind.config.js
+++ b/packages/challenger-ui/tailwind.config.js
@@ -1,4 +1,18 @@
-/** @type {import('tailwindcss').Config} */
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
export default {
content: {
relative: true,
diff --git a/packages/challenger-ui/tsconfig.json b/packages/challenger-ui/tsconfig.json
new file mode 100644
index 000000000..9826fac07
--- /dev/null
+++ b/packages/challenger-ui/tsconfig.json
@@ -0,0 +1,46 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ "target": "ES2020",
+ "module": "Node16",
+ "lib": ["DOM", "ES2020"],
+ "allowJs": true /* Allow javascript files to be compiled. */,
+ // "checkJs": true, /* Report errors in .js files. */
+ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
+ "jsxFactory": "h",
+ "jsxFragmentFactory": "Fragment",
+ "noEmit": true /* Do not emit outputs. */,
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+ /* Strict Type-Checking Options */
+ "strict": true /* Enable all strict type-checking options. */,
+ "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ /* Module Resolution Options */
+ "moduleResolution": "Node16" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+ "esModuleInterop": true /* */,
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ /* Source Map Options */
+ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ /* Advanced Options */
+ "skipLibCheck": true /* Skip type checking of declaration files. */
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index 3a9049f90..376265c0f 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/idb-bridge",
- "version": "0.0.16",
+ "version": "0.10.7",
"description": "IndexedDB implementation that uses SQLite3 as storage",
"main": "./dist/idb-bridge.js",
"module": "./lib/index.js",
diff --git a/packages/merchant-backend-ui/package.json b/packages/merchant-backend-ui/package.json
index f7306baf8..bd16317f5 100644
--- a/packages/merchant-backend-ui/package.json
+++ b/packages/merchant-backend-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backend-ui",
- "version": "0.0.5",
+ "version": "0.10.7",
"license": "AGPL-3.0-or-later",
"scripts": {
"compile": "tsc && ./build.mjs",
@@ -42,7 +42,7 @@
},
"devDependencies": {
"@babel/core": "7.18.9",
- "@gnu-taler/pogen": "^0.0.5",
+ "@gnu-taler/pogen": "workspace:*",
"@linaria/babel-preset": "3.0.0-beta.22",
"@linaria/core": "3.0.0-beta.22",
"@linaria/react": "3.0.0-beta.22",
@@ -66,4 +66,4 @@
"tslib": "2.6.2",
"typescript": "5.3.3"
}
-} \ No newline at end of file
+}
diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json
index b00133251..e80604777 100644
--- a/packages/merchant-backoffice-ui/package.json
+++ b/packages/merchant-backoffice-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backoffice-ui",
- "version": "0.9.3-dev.27",
+ "version": "0.10.7",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
@@ -37,9 +37,8 @@
"@typescript-eslint/parser": "^6.19.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.33.2",
-
"@creativebulma/bulma-tooltip": "^1.2.0",
- "@gnu-taler/pogen": "^0.0.5",
+ "@gnu-taler/pogen": "workspace:*",
"@types/chai": "^4.3.0",
"@types/history": "^4.7.8",
"@types/mocha": "^8.2.3",
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx
index 497f49c0e..097e98567 100644
--- a/packages/merchant-backoffice-ui/src/Application.tsx
+++ b/packages/merchant-backoffice-ui/src/Application.tsx
@@ -19,27 +19,68 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerMerchantApi, assertUnreachable, canonicalizeBaseUrl } from "@gnu-taler/taler-util";
+import {
+ CacheEvictor,
+ TalerMerchantApi,
+ TalerMerchantInstanceCacheEviction,
+ TalerMerchantManagementCacheEviction,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+} from "@gnu-taler/taler-util";
import {
BrowserHashNavigationProvider,
ConfigResultFail,
MerchantApiProvider,
TalerWalletIntegrationBrowserProvider,
TranslationProvider,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { SWRConfig } from "swr";
import { Routing } from "./Routing.js";
import { Loading } from "./components/exception/loading.js";
+import { NotificationCard } from "./components/menu/index.js";
import { SettingsProvider } from "./context/settings.js";
+import {
+ revalidateBankAccountDetails,
+ revalidateInstanceBankAccounts,
+} from "./hooks/bank.js";
+import {
+ revalidateBackendInstances,
+ revalidateInstanceDetails,
+ revalidateManagedInstanceDetails,
+} from "./hooks/instance.js";
+import {
+ revalidateInstanceOtpDevices,
+ revalidateOtpDeviceDetails,
+} from "./hooks/otp.js";
+import {
+ revalidateInstanceProducts,
+ revalidateProductDetails,
+} from "./hooks/product.js";
+import {
+ revalidateInstanceTemplates,
+ revalidateTemplateDetails,
+} from "./hooks/templates.js";
+import { revalidateInstanceTransfers } from "./hooks/transfer.js";
+import {
+ revalidateInstanceWebhooks,
+ revalidateWebhookDetails,
+} from "./hooks/webhooks.js";
import { strings } from "./i18n/strings.js";
-import { MerchantUiSettings, buildDefaultBackendBaseURL, fetchSettings } from "./settings.js";
-import { NotificationCard } from "./components/menu/index.js";
+import {
+ MerchantUiSettings,
+ buildDefaultBackendBaseURL,
+ fetchSettings,
+} from "./settings.js";
+import {
+ revalidateInstanceOrders,
+ revalidateOrderDetails,
+} from "./hooks/order.js";
+import { SessionContextProvider } from "./context/session.js";
const WITH_LOCAL_STORAGE_CACHE = false;
-
export function Application(): VNode {
const [settings, setSettings] = useState<MerchantUiSettings>();
useEffect(() => {
@@ -57,40 +98,48 @@ export function Application(): VNode {
de: strings["de"].completeness,
}}
>
- <MerchantApiProvider baseUrl={new URL("/", baseUrl)} frameOnError={OnConfigError}>
- <SWRConfig
- value={{
- provider: WITH_LOCAL_STORAGE_CACHE
- ? localStorageProvider
- : undefined,
- // normally, do not revalidate
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- revalidateIfStale: false,
- revalidateOnMount: undefined,
- focusThrottleInterval: undefined,
-
- // normally, do not refresh
- refreshInterval: undefined,
- dedupingInterval: 2000,
- refreshWhenHidden: false,
- refreshWhenOffline: false,
-
- // ignore errors
- shouldRetryOnError: false,
- errorRetryCount: 0,
- errorRetryInterval: undefined,
-
- // do not go to loading again if already has data
- keepPreviousData: true,
- }}
- >
- <TalerWalletIntegrationBrowserProvider>
- <BrowserHashNavigationProvider>
- <Routing />
- </BrowserHashNavigationProvider>
- </TalerWalletIntegrationBrowserProvider>
- </SWRConfig>
+ <MerchantApiProvider
+ baseUrl={new URL("./", baseUrl)}
+ frameOnError={OnConfigError}
+ evictors={{
+ management: swrCacheEvictor,
+ }}
+ >
+ <SessionContextProvider>
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <TalerWalletIntegrationBrowserProvider>
+ <BrowserHashNavigationProvider>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </TalerWalletIntegrationBrowserProvider>
+ </SWRConfig>
+ </SessionContextProvider>
</MerchantApiProvider>
</TranslationProvider>
</SettingsProvider>
@@ -141,31 +190,175 @@ function localStorageProvider(): Map<unknown, unknown> {
return map;
}
-function OnConfigError({ state }: { state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined }): VNode {
+function OnConfigError({
+ state,
+}: {
+ state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined;
+}): VNode {
const { i18n } = useTranslationContext();
if (!state) {
- return <i18n.Translate>checking compatibility with server...</i18n.Translate>
+ return (
+ <i18n.Translate>checking compatibility with server...</i18n.Translate>
+ );
}
switch (state.type) {
case "error": {
- return <NotificationCard
- notification={{
- message: i18n.str`Contacting the server failed`,
- description: state.error.message,
- details: JSON.stringify(state.error.errorDetail, undefined, 2),
- type: "ERROR",
- }}
- />
+ return (
+ <NotificationCard
+ notification={{
+ message: i18n.str`Contacting the server failed`,
+ description: state.error.message,
+ details: JSON.stringify(state.error.errorDetail, undefined, 2),
+ type: "ERROR",
+ }}
+ />
+ );
}
case "incompatible": {
- return <NotificationCard
- notification={{
- message: i18n.str`The server version is not supported`,
- description: i18n.str`Supported version "${state.supported}", server version "${state.result.version}".`,
- type: "WARN",
- }}
- />
+ return (
+ <NotificationCard
+ notification={{
+ message: i18n.str`The server version is not supported`,
+ description: i18n.str`Supported version "${state.supported}", server version "${state.result.version}".`,
+ type: "WARN",
+ }}
+ />
+ );
}
- default: assertUnreachable(state)
+ default:
+ assertUnreachable(state);
}
}
+
+const swrCacheEvictor = new (class
+ implements
+ CacheEvictor<
+ TalerMerchantManagementCacheEviction | TalerMerchantInstanceCacheEviction
+ >
+{
+ async notifySuccess(
+ op:
+ | TalerMerchantManagementCacheEviction
+ | TalerMerchantInstanceCacheEviction,
+ ) {
+ switch (op) {
+ case TalerMerchantManagementCacheEviction.CREATE_INSTANCE: {
+ await Promise.all([revalidateBackendInstances()]);
+ return;
+ }
+ case TalerMerchantManagementCacheEviction.UPDATE_INSTANCE: {
+ await Promise.all([revalidateManagedInstanceDetails()]);
+ return;
+ }
+ case TalerMerchantManagementCacheEviction.DELETE_INSTANCE: {
+ await Promise.all([revalidateBackendInstances()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_CURRENT_INSTANCE: {
+ await Promise.all([revalidateInstanceDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_CURRENT_INSTANCE: {
+ await Promise.all([revalidateInstanceDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_BANK_ACCOUNT: {
+ await Promise.all([revalidateInstanceBankAccounts()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_BANK_ACCOUNT: {
+ await Promise.all([revalidateBankAccountDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_BANK_ACCOUNT: {
+ await Promise.all([revalidateInstanceBankAccounts()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_PRODUCT: {
+ await Promise.all([revalidateInstanceProducts()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT: {
+ await Promise.all([revalidateProductDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_PRODUCT: {
+ await Promise.all([revalidateInstanceProducts()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_TRANSFER: {
+ await Promise.all([revalidateInstanceTransfers()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_TRANSFER: {
+ await Promise.all([revalidateInstanceTransfers()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_DEVICE: {
+ await Promise.all([revalidateInstanceOtpDevices()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_DEVICE: {
+ await Promise.all([revalidateOtpDeviceDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_DEVICE: {
+ await Promise.all([revalidateInstanceOtpDevices()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_TEMPLATE: {
+ await Promise.all([revalidateInstanceTemplates()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_TEMPLATE: {
+ await Promise.all([revalidateTemplateDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_TEMPLATE: {
+ await Promise.all([revalidateInstanceTemplates()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_WEBHOOK: {
+ await Promise.all([revalidateInstanceWebhooks()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_WEBHOOK: {
+ await Promise.all([revalidateWebhookDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_WEBHOOK: {
+ await Promise.all([revalidateInstanceWebhooks()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.CREATE_ORDER: {
+ await Promise.all([revalidateInstanceOrders()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.UPDATE_ORDER: {
+ await Promise.all([revalidateOrderDetails()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.DELETE_ORDER: {
+ await Promise.all([revalidateInstanceOrders()]);
+ return;
+ }
+ case TalerMerchantInstanceCacheEviction.LAST:
+ // case TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY:{
+ // await Promise.all([
+ // reva
+ // ])
+ // return
+ // }
+ // case TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY:{
+ // await Promise.all([
+ // ])
+ // return
+ // }
+ // case TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY:{
+ // await Promise.all([
+ // ])
+ // return
+ // }
+ }
+ }
+})();
diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx
index c30b1912a..665137415 100644
--- a/packages/merchant-backoffice-ui/src/Routing.tsx
+++ b/packages/merchant-backoffice-ui/src/Routing.tsx
@@ -19,14 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AbsoluteTime, TalerErrorDetail, TranslatedString } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- urlPattern,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, FunctionComponent, VNode, h } from "preact";
+ AbsoluteTime,
+ TalerError,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { urlPattern, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { createHashHistory } from "history";
+import { Fragment, VNode, h } from "preact";
import { Route, Router, route } from "preact-router";
import { useEffect, useErrorBoundary, useState } from "preact/hooks";
import { Loading } from "./components/exception/loading.js";
@@ -35,13 +35,10 @@ import {
NotConnectedAppMenu,
NotificationCard,
} from "./components/menu/index.js";
+import { useSessionContext } from "./context/session.js";
import { useInstanceBankAccounts } from "./hooks/bank.js";
import { useInstanceKYCDetails } from "./hooks/instance.js";
import { usePreference } from "./hooks/preference.js";
-import {
- DEFAULT_ADMIN_USERNAME,
- useSessionContext,
-} from "./context/session.js";
import InstanceCreatePage from "./paths/admin/create/index.js";
import InstanceListPage from "./paths/admin/list/index.js";
import BankAccountCreatePage from "./paths/instance/accounts/create/index.js";
@@ -73,10 +70,9 @@ import WebhookCreatePage from "./paths/instance/webhooks/create/index.js";
import WebhookListPage from "./paths/instance/webhooks/list/index.js";
import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js";
import { LoginPage } from "./paths/login/index.js";
-import NotFoundPage from "./paths/notfound/index.js";
+import { NotFoundPage } from "./paths/notfound/index.js";
import { Settings } from "./paths/settings/index.js";
import { Notification } from "./utils/types.js";
-import { createHashHistory } from "history";
export enum InstancePaths {
error = "/error",
@@ -122,7 +118,7 @@ export enum InstancePaths {
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
-const noop = () => {};
+// const noop = () => { };
export enum AdminPaths {
list_instances = "/instances",
@@ -143,7 +139,7 @@ export const publicPages = {
const history = createHashHistory();
export function Routing(_p: Props): VNode {
- const { i18n } = useTranslationContext();
+ // const { i18n } = useTranslationContext();
const { state } = useSessionContext();
type GlobalNotifState =
@@ -153,84 +149,94 @@ export function Routing(_p: Props): VNode {
useState<GlobalNotifState>(undefined);
const [error] = useErrorBoundary();
+ const [preference] = usePreference();
+
+ const now = AbsoluteTime.now();
const instance = useInstanceBankAccounts();
- const accounts = !instance.ok ? undefined : instance.data.accounts;
+ const accounts =
+ !instance || instance instanceof TalerError || instance.type === "fail"
+ ? undefined
+ : instance.body;
const shouldWarnAboutMissingBankAccounts =
- !state.isAdmin && accounts !== undefined && accounts.length < 1;
- const shouldLogin =
- state.status === "loggedOut" || state.status === "expired";
-
- function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) {
- return function ServerErrorRedirectToImpl(
- error: HttpError<TalerErrorDetail>,
- ) {
- if (error.type === ErrorType.TIMEOUT) {
- setGlobalNotification({
- message: i18n.str`The request to the backend take too long and was cancelled`,
- description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`,
- type: "ERROR",
- to,
- });
- } else {
- setGlobalNotification({
- message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
- description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
- details:
- error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER
- ? error.payload.hint
- : undefined,
- type: "ERROR",
- to,
- });
- }
- return <Redirect to={to} />;
- };
- }
+ !state.isAdmin &&
+ accounts !== undefined &&
+ accounts.accounts.length < 1 &&
+ (AbsoluteTime.isNever(preference.hideMissingAccountUntil) ||
+ AbsoluteTime.cmp(now, preference.hideMissingAccountUntil) > 1);
+
+ const shouldLogin = state.status === "loggedOut";
+
+ // function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) {
+ // return function ServerErrorRedirectToImpl(
+ // error: HttpError<TalerErrorDetail>,
+ // ) {
+ // if (error.type === ErrorType.TIMEOUT) {
+ // setGlobalNotification({
+ // message: i18n.str`The request to the backend take too long and was cancelled`,
+ // description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`,
+ // type: "ERROR",
+ // to,
+ // });
+ // } else {
+ // setGlobalNotification({
+ // message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
+ // description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ // details:
+ // error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER
+ // ? error.payload.hint
+ // : undefined,
+ // type: "ERROR",
+ // to,
+ // });
+ // }
+ // return <Redirect to={to} />;
+ // };
+ // }
// const LoginPageAccessDeniend = onUnauthorized
- const LoginPageAccessDenied = () => {
- return (
- <Fragment>
- <NotificationCard
- notification={{
- message: i18n.str`Access denied`,
- description: i18n.str`Session expired or password changed.`,
- type: "ERROR",
- }}
- />
- <LoginPage />
- </Fragment>
- );
- };
-
- function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<unknown>) {
- return function IfAdminCreateDefaultOrImpl(props?: T) {
- if (state.isAdmin && state.instance === DEFAULT_ADMIN_USERNAME) {
- return (
- <Fragment>
- <NotificationCard
- notification={{
- message: i18n.str`No 'default' instance configured yet.`,
- description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`,
- type: "INFO",
- }}
- />
- <InstanceCreatePage
- forceId={DEFAULT_ADMIN_USERNAME}
- onConfirm={() => {
- route(InstancePaths.bank_list);
- }}
- />
- </Fragment>
- );
- }
- if (props) {
- return <Next {...props} />;
- }
- return <Next />;
- };
- }
+ // const LoginPageAccessDenied = () => {
+ // return (
+ // <Fragment>
+ // <NotificationCard
+ // notification={{
+ // message: i18n.str`Access denied`,
+ // description: i18n.str`Session expired or password changed.`,
+ // type: "ERROR",
+ // }}
+ // />
+ // <LoginPage />
+ // </Fragment>
+ // );
+ // };
+
+ // function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<unknown>) {
+ // return function IfAdminCreateDefaultOrImpl(props?: T) {
+ // if (state.isAdmin && state.instance === DEFAULT_ADMIN_USERNAME) {
+ // return (
+ // <Fragment>
+ // <NotificationCard
+ // notification={{
+ // message: i18n.str`No 'default' instance configured yet.`,
+ // description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`,
+ // type: "INFO",
+ // }}
+ // />
+ // <InstanceCreatePage
+ // forceId={DEFAULT_ADMIN_USERNAME}
+ // onConfirm={() => {
+ // route(InstancePaths.bank_list);
+ // }}
+ // />
+ // </Fragment>
+ // );
+ // }
+ // if (props) {
+ // return <Next {...props} />;
+ // }
+ // return <Next />;
+ // };
+ // }
if (shouldLogin) {
return (
@@ -245,14 +251,12 @@ export function Routing(_p: Props): VNode {
return (
<Fragment>
<Menu />
- <NotificationCard
- notification={{
- type: "INFO",
- message: i18n.str`You need to associate a bank account to receive revenue.`,
- description: i18n.str`Without this the merchant backend will refuse to create new orders.`,
+ <BankAccountBanner />
+ <BankAccountCreatePage
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
}}
/>
- <BankAccountCreatePage onConfirm={() => {}} />
</Fragment>
);
}
@@ -304,8 +308,6 @@ export function Routing(_p: Props): VNode {
onUpdate={(id: string): void => {
route(`/instance/${id}/update`);
}}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
/>
)}
{state.isAdmin && (
@@ -314,7 +316,7 @@ export function Routing(_p: Props): VNode {
component={InstanceCreatePage}
onBack={() => route(AdminPaths.list_instances)}
onConfirm={() => {
- route(InstancePaths.order_list);
+ route(AdminPaths.list_instances);
}}
/>
)}
@@ -326,9 +328,6 @@ export function Routing(_p: Props): VNode {
onConfirm={() => {
route(AdminPaths.list_instances);
}}
- onUpdateError={ServerErrorRedirectTo(AdminPaths.list_instances)}
- onLoadError={ServerErrorRedirectTo(AdminPaths.list_instances)}
- onNotFound={NotFoundPage}
/>
)}
{/**
@@ -343,10 +342,6 @@ export function Routing(_p: Props): VNode {
onConfirm={() => {
route(`/`);
}}
- onUpdateError={noop}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
/>
{/**
* Update instance page
@@ -360,9 +355,6 @@ export function Routing(_p: Props): VNode {
onCancel={() => {
route(InstancePaths.order_list);
}}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
/>
{/**
* Inventory pages
@@ -370,28 +362,22 @@ export function Routing(_p: Props): VNode {
<Route
path={InstancePaths.inventory_list}
component={ProductListPage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.inventory_new);
}}
onSelect={(id: string) => {
route(InstancePaths.inventory_update.replace(":pid", id));
}}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
<Route
path={InstancePaths.inventory_update}
component={ProductUpdatePage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)}
onConfirm={() => {
route(InstancePaths.inventory_list);
}}
onBack={() => {
route(InstancePaths.inventory_list);
}}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
<Route
path={InstancePaths.inventory_new}
@@ -409,28 +395,22 @@ export function Routing(_p: Props): VNode {
<Route
path={InstancePaths.bank_list}
component={BankAccountListPage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.bank_new);
}}
onSelect={(id: string) => {
route(InstancePaths.bank_update.replace(":bid", id));
}}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
<Route
path={InstancePaths.bank_update}
component={BankAccountUpdatePage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)}
onConfirm={() => {
route(InstancePaths.bank_list);
}}
onBack={() => {
route(InstancePaths.bank_list);
}}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
<Route
path={InstancePaths.bank_new}
@@ -454,16 +434,10 @@ export function Routing(_p: Props): VNode {
onSelect={(id: string) => {
route(InstancePaths.order_details.replace(":oid", id));
}}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
<Route
path={InstancePaths.order_details}
component={OrderDetailsPage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.order_list)}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onBack={() => {
route(InstancePaths.order_list);
}}
@@ -484,9 +458,6 @@ export function Routing(_p: Props): VNode {
<Route
path={InstancePaths.transfers_list}
component={TransferListPage}
- onUnauthorized={LoginPageAccessDenied}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.transfers_new);
}}
@@ -507,9 +478,6 @@ export function Routing(_p: Props): VNode {
<Route
path={InstancePaths.webhooks_list}
component={WebhookListPage}
- onUnauthorized={LoginPageAccessDenied}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.webhooks_new);
}}
@@ -523,9 +491,6 @@ export function Routing(_p: Props): VNode {
onConfirm={() => {
route(InstancePaths.webhooks_list);
}}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.webhooks_list)}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onBack={() => {
route(InstancePaths.webhooks_list);
}}
@@ -546,9 +511,6 @@ export function Routing(_p: Props): VNode {
<Route
path={InstancePaths.otp_devices_list}
component={ValidatorListPage}
- onUnauthorized={LoginPageAccessDenied}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.otp_devices_new);
}}
@@ -562,9 +524,6 @@ export function Routing(_p: Props): VNode {
onConfirm={() => {
route(InstancePaths.otp_devices_list);
}}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.otp_devices_list)}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onBack={() => {
route(InstancePaths.otp_devices_list);
}}
@@ -585,9 +544,6 @@ export function Routing(_p: Props): VNode {
<Route
path={InstancePaths.templates_list}
component={TemplateListPage}
- onUnauthorized={LoginPageAccessDenied}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.templates_new);
}}
@@ -607,9 +563,6 @@ export function Routing(_p: Props): VNode {
onConfirm={() => {
route(InstancePaths.templates_list);
}}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onBack={() => {
route(InstancePaths.templates_list);
}}
@@ -630,9 +583,6 @@ export function Routing(_p: Props): VNode {
onOrderCreated={(id: string) => {
route(InstancePaths.order_details.replace(":oid", id));
}}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onBack={() => {
route(InstancePaths.templates_list);
}}
@@ -640,9 +590,6 @@ export function Routing(_p: Props): VNode {
<Route
path={InstancePaths.templates_qr}
component={TemplateQrPage}
- onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
- onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onBack={() => {
route(InstancePaths.templates_list);
}}
@@ -671,57 +618,93 @@ function AdminInstanceUpdatePage({
id,
...rest
}: { id: string } & InstanceUpdatePageProps): VNode {
- const { i18n } = useTranslationContext();
+ // const { i18n } = useTranslationContext();
return (
<Fragment>
<InstanceAdminUpdatePage
{...rest}
instanceId={id}
- onLoadError={(error: HttpError<TalerErrorDetail>) => {
- const notif =
- error.type === ErrorType.TIMEOUT
- ? {
- message: i18n.str`The request to the backend take too long and was cancelled`,
- description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
- type: "ERROR" as const,
- }
- : {
- message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
- description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
- details:
- error.type === ErrorType.CLIENT ||
- error.type === ErrorType.SERVER
- ? error.payload.hint
- : undefined,
- type: "ERROR" as const,
- };
- return (
- <Fragment>
- <NotificationCard notification={notif} />
- <LoginPage />
- </Fragment>
- );
- }}
- onUnauthorized={() => {
- return (
- <Fragment>
- <NotificationCard
- notification={{
- message: i18n.str`Access denied`,
- description: i18n.str`The access token provided is invalid`,
- type: "ERROR",
- }}
- />
- <LoginPage />
- </Fragment>
- );
- }}
+ // onLoadError={(error: HttpError<TalerErrorDetail>) => {
+ // const notif =
+ // error.type === ErrorType.TIMEOUT
+ // ? {
+ // message: i18n.str`The request to the backend take too long and was cancelled`,
+ // description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ // type: "ERROR" as const,
+ // }
+ // : {
+ // message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
+ // description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
+ // details:
+ // error.type === ErrorType.CLIENT ||
+ // error.type === ErrorType.SERVER
+ // ? error.payload.hint
+ // : undefined,
+ // type: "ERROR" as const,
+ // };
+ // return (
+ // <Fragment>
+ // <NotificationCard notification={notif} />
+ // <LoginPage />
+ // </Fragment>
+ // );
+ // }}
+ // onUnauthorized={() => {
+ // return (
+ // <Fragment>
+ // <NotificationCard
+ // notification={{
+ // message: i18n.str`Access denied`,
+ // description: i18n.str`The access token provided is invalid`,
+ // type: "ERROR",
+ // }}
+ // />
+ // <LoginPage />
+ // </Fragment>
+ // );
+ // }}
/>
</Fragment>
);
}
+function BankAccountBanner(): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [, updatePref] = usePreference();
+ const now = AbsoluteTime.now();
+ const oneDay = { d_ms: 1000 * 60 * 60 * 24 };
+ const tomorrow = AbsoluteTime.addDuration(now, oneDay);
+
+ return (
+ <NotificationCard
+ notification={{
+ type: "INFO",
+ message: i18n.str`You need to associate a bank account to receive revenue.`,
+ description: (
+ <div>
+ <p>
+ <i18n.Translate>
+ Without this the merchant backend will refuse to create new
+ orders.
+ </i18n.Translate>
+ </p>
+ <div class="buttons is-right">
+ <button
+ class="button"
+ onClick={() => updatePref("hideMissingAccountUntil", tomorrow)}
+ >
+ <i18n.Translate>Hide for today</i18n.Translate>
+ </button>
+ </div>
+ </div>
+ ),
+ }}
+ />
+ );
+}
+
function KycBanner(): VNode {
const kycStatus = useInstanceKYCDetails();
const { i18n } = useTranslationContext();
@@ -730,7 +713,11 @@ function KycBanner(): VNode {
const now = AbsoluteTime.now();
- const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
+ const needsToBeShown =
+ kycStatus !== undefined &&
+ !(kycStatus instanceof TalerError) &&
+ kycStatus.type === "ok" &&
+ !!kycStatus.body;
const hidden = AbsoluteTime.cmp(now, prefs.hideKycUntil) < 1;
if (hidden || !needsToBeShown) return <Fragment />;
@@ -746,8 +733,10 @@ function KycBanner(): VNode {
description: (
<div>
<p>
- Some transfer are on hold until a KYC process is completed. Go to
- the KYC section in the left panel for more information
+ <i18n.Translate>
+ Some transfer are on hold until a KYC process is completed. Go
+ to the KYC section in the left panel for more information
+ </i18n.Translate>
</p>
<div class="buttons is-right">
<button
diff --git a/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx b/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx
new file mode 100644
index 000000000..b1d1cac66
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx
@@ -0,0 +1,146 @@
+/*
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { TalerError, TalerErrorCode, assertUnreachable } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { NotificationCard } from "./menu/index.js";
+
+/**
+ * equivalent to ErrorLoading for merchant-backoffice which uses notification-card
+ * @param param0
+ * @returns
+ */
+export function ErrorLoadingMerchant({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode {
+ const { i18n } = useTranslationContext()
+ switch (error.errorDetail.code) {
+ //////////////////
+ // Every error that can be produce in a Http Request
+ //////////////////
+ case TalerErrorCode.GENERIC_TIMEOUT: {
+ if (error.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The request reached a timeout, check your connection.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The request was cancelled.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The request reached a timeout, check your connection.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) {
+ const { requestMethod, requestUrl, throttleStats } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`A lot of request were made to the same server and this action was throttled.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) {
+ const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`The response of the request is malformed.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) {
+ const { requestMethod, requestUrl } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`Could not complete the request due to a network problem.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) {
+ const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`Unexpected request error.`,
+ description: error.message,
+ details: JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)
+ }} />
+ }
+ assertUnreachable(1 as never)
+ }
+ //////////////////
+ // Every other error
+ //////////////////
+ // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ // return <Attention type="danger" title={i18n.str``}>
+ // </Attention>
+ // }
+ //////////////////
+ // Default message for unhandled case
+ //////////////////
+ default: {
+ return <NotificationCard
+ notification={{
+ type: "ERROR",
+ message: i18n.str`Unexpected error.`,
+ description: error.message,
+ details: JSON.stringify(error.errorDetail, undefined, 2)
+ }} />
+ }
+ }
+}
+
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
index 76d38db84..11396b88e 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
@@ -23,6 +23,7 @@ import { ComponentChildren, h, VNode } from "preact";
import { InputWithAddon } from "./InputWithAddon.js";
import { InputProps } from "./useField.js";
import { AmountString } from "@gnu-taler/taler-util";
+import { useSessionContext } from "../../context/session.js";
export interface Props<T> extends InputProps<T> {
expand?: boolean;
@@ -43,7 +44,7 @@ export function InputCurrency<T>({
children,
side,
}: Props<keyof T>): VNode {
- const { config } = useMerchantApiContext();
+ const { config } = useSessionContext();
return (
<InputWithAddon<T>
name={name}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx b/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx
index 10b28cd93..38444b85d 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx
@@ -53,8 +53,9 @@ export function InputNumber<T>({
help={help}
tooltip={tooltip}
inputExtra={{ min: 0 }}
- children={children}
side={side}
- />
+ >
+ {children}
+ </InputWithAddon>
);
}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
index 3337e5f57..a0c15c77c 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -18,7 +18,11 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import {
+ parsePaytoUri,
+ PaytoUriGeneric,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { COUNTRY_TABLE } from "../../utils/constants.js";
@@ -71,7 +75,7 @@ function checkAddressChecksum(address: string) {
return true;
}
-function validateBitcoin(
+function validateBitcoin_path1(
addr: string,
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): string | undefined {
@@ -84,7 +88,7 @@ function validateBitcoin(
return i18n.str`This is not a valid bitcoin address.`;
}
-function validateEthereum(
+function validateEthereum_path1(
addr: string,
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): string | undefined {
@@ -98,6 +102,29 @@ function validateEthereum(
}
/**
+ * validates
+ * bank.com/
+ * bank.com
+ * bank.com/path
+ * bank.com/path/subpath/
+ */
+const DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+(\/[a-zA-Z0-9-.]+)*\/?$/
+
+function validateTalerBank_path1(
+ addr: string,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): string | undefined {
+ console.log(addr, DOMAIN_REGEX.test(addr))
+ try {
+ const valid = DOMAIN_REGEX.test(addr);
+ if (valid) return undefined;
+ } catch (e) {
+ console.log(e);
+ }
+ return i18n.str`This is not a valid host.`;
+}
+
+/**
* An IBAN is validated by converting it into an integer and performing a
* basic mod-97 operation (as described in ISO 7064) on it.
* If the IBAN is valid, the remainder equals 1.
@@ -111,7 +138,7 @@ function validateEthereum(
* If the remainder is 1, the check digit test is passed and the IBAN might be valid.
*
*/
-function validateIBAN(
+function validateIBAN_path1(
iban: string,
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): string | undefined {
@@ -178,34 +205,36 @@ export function InputPaytoForm<T>({
}: Props<keyof T>): VNode {
const { value: initialValueStr, onChange } = useField<T>(name);
- const initialPayto = parsePaytoUri(initialValueStr ?? "")
- const paths = !initialPayto ? [] : initialPayto.targetPath.split("/")
+ const initialPayto = parsePaytoUri(initialValueStr ?? "");
+ const paths = !initialPayto ? [] : initialPayto.targetPath.split("/");
const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
const initialPath2 = paths.length >= 2 ? paths[1] : undefined;
- const initial: Entity = initialPayto === undefined ? defaultTarget : {
- target: initialPayto.targetType,
- params: initialPayto.params,
- path1: initialPath1,
- path2: initialPath2,
- }
- const [value, setValue] = useState<Partial<Entity>>(initial)
+ const initial: Entity =
+ initialPayto === undefined
+ ? defaultTarget
+ : {
+ target: initialPayto.targetType,
+ params: initialPayto.params,
+ path1: initialPath1,
+ path2: initialPath2,
+ };
+ const [value, setValue] = useState<Partial<Entity>>(initial);
const { i18n } = useTranslationContext();
const errors: FormErrors<Entity> = {
- target:
- value.target === noTargetValue
- ? i18n.str`required`
- : undefined,
+ target: value.target === noTargetValue ? i18n.str`required` : undefined,
path1: !value.path1
? i18n.str`required`
: value.target === "iban"
- ? validateIBAN(value.path1, i18n)
+ ? validateIBAN_path1(value.path1, i18n)
: value.target === "bitcoin"
- ? validateBitcoin(value.path1, i18n)
+ ? validateBitcoin_path1(value.path1, i18n)
: value.target === "ethereum"
- ? validateEthereum(value.path1, i18n)
- : undefined,
+ ? validateEthereum_path1(value.path1, i18n)
+ : value.target === "x-taler-bank"
+ ? validateTalerBank_path1(value.path1, i18n)
+ : undefined,
path2:
value.target === "x-taler-bank"
? !value.path2
@@ -222,15 +251,22 @@ export function InputPaytoForm<T>({
const hasErrors = Object.keys(errors).some(
(k) => (errors as any)[k] !== undefined,
);
- const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({
- targetType: value.target,
- targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""),
- params: value.params ?? {} as any,
- isKnown: false,
- })
+
+ const path1WithSlash = value.path1 && !value.path1.endsWith("/") ? value.path1 + "/" : value.path1
+ const str =
+ hasErrors || !value.target
+ ? undefined
+ : stringifyPaytoUri({
+ targetType: value.target,
+ targetPath: value.path2
+ ? `${path1WithSlash}${value.path2}`
+ : value.path1 ?? "",
+ params: value.params ?? ({} as any),
+ isKnown: false,
+ });
useEffect(() => {
- onChange(str as any)
- }, [str])
+ onChange(str as any);
+ }, [str]);
// const submit = useCallback((): void => {
// // const accounts: TalerMerchantApi.AccountAddDetails[] = paytos;
@@ -365,7 +401,23 @@ export function InputPaytoForm<T>({
name="path1"
readonly={readonly}
label={i18n.str`Host`}
+ fromStr={(v) => {
+ if (v.startsWith("http")) {
+ try {
+ const url = new URL(v);
+ return url.host + url.pathname;
+ } catch {
+ return v;
+ }
+ }
+ return v;
+ }}
tooltip={i18n.str`Bank host.`}
+ help={<Fragment>
+ <div><i18n.Translate>Without scheme and may include subpath:</i18n.Translate></div>
+ <div>bank.com/</div>
+ <div>bank.com/path/subpath/</div>
+ </Fragment>}
/>
<Input<Entity>
name="path2"
@@ -389,9 +441,7 @@ export function InputPaytoForm<T>({
/>
</Fragment>
)}
-
</FormProvider>
</InputGroup>
);
}
-
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
index 89b815b4b..8c935f33b 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
@@ -79,7 +79,7 @@ export function InputToggle<T>({
disabled={readonly}
onChange={onCheckboxClick}
/>
- <div class="toggle-switch"></div>
+ <div class={`toggle-switch ${readonly ? "disabled" : ""}`} style={{ cursor: readonly ? "default" : undefined }}></div>
</label>
{help}
</p>
diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx
index a0e1d6ae4..f5f9d5b4f 100644
--- a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx
@@ -3,7 +3,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-export function JumpToElementById({ testIfExist, onSelect, placeholder, description }: { placeholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<any>, onSelect: (id: string) => void }): VNode {
+export function JumpToElementById({ testIfExist, onSelect, placeholder, description }: { placeholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<boolean>, onSelect: (id: string) => void }): VNode {
const { i18n } = useTranslationContext()
const [error, setError] = useState<string | undefined>(
@@ -17,9 +17,13 @@ export function JumpToElementById({ testIfExist, onSelect, placeholder, descript
return;
}
try {
- await testIfExist(currentId);
- onSelect(currentId);
- setError(undefined);
+ const exi = await testIfExist(currentId);
+ if (exi) {
+ onSelect(currentId);
+ setError(undefined);
+ } else {
+ setError(i18n.str`not found`);
+ }
} catch {
setError(i18n.str`not found`);
}
diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
index cb4442897..864d09f48 100644
--- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -19,9 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useSessionContext } from "../../context/session.js";
import { Entity } from "../../paths/admin/create/CreatePage.js";
@@ -33,6 +31,7 @@ import { InputLocation } from "../form/InputLocation.js";
import { InputSelector } from "../form/InputSelector.js";
import { InputToggle } from "../form/InputToggle.js";
import { InputWithAddon } from "../form/InputWithAddon.js";
+import { TextField } from "../form/TextField.js";
export function DefaultInstanceFormFields({
readonlyId,
@@ -42,15 +41,13 @@ export function DefaultInstanceFormFields({
showId: boolean;
}): VNode {
const { i18n } = useTranslationContext();
- const {
- state: { backendUrl },
- } = useSessionContext();
+ const { state } = useSessionContext();
return (
<Fragment>
{showId && (
<InputWithAddon<Entity>
name="id"
- addonBefore={new URL("instances/", backendUrl).href}
+ addonBefore={new URL("instances/", state.backendUrl.href).href}
readonly={readonlyId}
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`}
@@ -63,11 +60,20 @@ export function DefaultInstanceFormFields({
tooltip={i18n.str`Legal name of the business represented by this instance.`}
/>
+ <TextField name="asdasd" label="">
+ <i18n.Translate>
+ Choose individual if you don't have or are not required to have legal business permission.
+ </i18n.Translate>
+ </TextField>
+
<InputSelector<Entity>
name="user_type"
- label={i18n.str`Type`}
+ label={i18n.str`Selling as`}
tooltip={i18n.str`Different type of account can have different rules and requirements.`}
values={["business", "individual"]}
+ toStr={(d: string) => {
+ return d.toUpperCase();
+ }}
/>
<Input<Entity>
@@ -88,12 +94,6 @@ export function DefaultInstanceFormFields({
tooltip={i18n.str`Logo image.`}
/>
- <InputToggle<Entity>
- name="use_stefan"
- label={i18n.str`Pay transaction fee`}
- tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`}
- />
-
<InputGroup
name="address"
label={i18n.str`Address`}
@@ -110,6 +110,12 @@ export function DefaultInstanceFormFields({
<InputLocation name="jurisdiction" />
</InputGroup>
+ <InputToggle<Entity>
+ name="use_stefan"
+ label={i18n.str`Pay transaction fee`}
+ tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`}
+ />
+
<InputDuration<Entity>
name="default_pay_delay"
label={i18n.str`Default payment delay`}
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index adc47b216..2090704d9 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -19,10 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import {
- useMerchantApiContext,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
+import { TalerError } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useSessionContext } from "../../context/session.js";
import { useInstanceKYCDetails } from "../../hooks/instance.js";
@@ -37,14 +35,17 @@ interface Props {
export function Sidebar({ mobile }: Props): VNode {
const { i18n } = useTranslationContext();
+ const { state, logOut, config } = useSessionContext();
const kycStatus = useInstanceKYCDetails();
- const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
- const { state, logOut } = useSessionContext();
+
+ const needKYC =
+ kycStatus !== undefined &&
+ !(kycStatus instanceof TalerError) &&
+ kycStatus.type === "ok" &&
+ !!kycStatus.body;
const isLoggedIn = state.status === "loggedIn";
const hasToken = isLoggedIn && state.token !== undefined;
- const backendURL = state.backendUrl;
- const { config } = useMerchantApiContext();
-
+
return (
<aside
class="aside is-placed-left is-expanded"
@@ -190,12 +191,7 @@ export function Sidebar({ mobile }: Props): VNode {
</p>
<ul class="menu-list">
<li>
- <a
- class="has-icon is-state-info is-hoverable"
- onClick={(e): void => {
- e.preventDefault();
- }}
- >
+ <a class="has-icon is-state-info is-hoverable" href="/interface">
<span class="icon">
<i class="mdi mdi-newspaper" />
</span>
@@ -209,9 +205,7 @@ export function Sidebar({ mobile }: Props): VNode {
<span style={{ width: "3rem" }} class="icon">
<i class="mdi mdi-web" />
</span>
- <span class="menu-item-label">
- {new URL(backendURL).hostname}
- </span>
+ <span class="menu-item-label">{state.backendUrl.hostname}</span>
</div>
</li>
<li>
diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
index aa955db4e..a35c07ace 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -104,7 +104,7 @@ export function Menu(_p: MenuProps): VNode {
? getInstanceTitle(path, state.instance)
: getAdminTitle(path, state.instance);
- const isLoggedIn =state.status === "loggedIn";
+ const isLoggedIn = state.status === "loggedIn";
return (
<WithTitle title={titleWithSubtitle}>
@@ -117,11 +117,9 @@ export function Menu(_p: MenuProps): VNode {
title={titleWithSubtitle}
/>
- {isLoggedIn && (
- <Sidebar mobile={mobileOpen} />
- )}
+ {isLoggedIn && <Sidebar mobile={mobileOpen} />}
- {state.status !== "loggedOut" && state.impersonate !== undefined && (
+ {state.status !== "loggedOut" && state.impersonated && (
<nav
class="level"
style={{
@@ -137,9 +135,8 @@ export function Menu(_p: MenuProps): VNode {
.{" "}
<a
href="#/instances"
- onClick={(e) => {
+ onClick={() => {
deImpersonate();
- e.preventDefault();
}}
>
go back
@@ -228,7 +225,7 @@ export function NotYetReadyAppMenu({ title }: NotYetReadyAppMenuProps): VNode {
useEffect(() => {
document.title = `Taler Backoffice: ${title}`;
}, [title]);
-
+
const isLoggedIn = state.status === "loggedIn";
return (
diff --git a/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx b/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx
index d637958cb..6dc1fadd6 100644
--- a/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx
+++ b/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, Component } from "preact";
+import { Component, h } from "preact";
interface Props {
closeFunction?: () => void;
@@ -64,7 +64,7 @@ export class DatePicker extends Component<Props, State> {
getDaysByMonth(month: number, year: number) {
const calendar = [];
- const date = new Date(year, month, 1); // month to display
+ // const date = new Date(year, month, 1); // month to display
const firstDay = new Date(year, month, 1).getDay(); // first weekday of month
const lastDate = new Date(year, month + 1, 0).getDate(); // last date of month
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
index 468e5f635..c6d687b85 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
@@ -19,9 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h } from "preact";
import { useCallback, useEffect, useState } from "preact/hooks";
import * as yup from "yup";
@@ -38,7 +37,6 @@ import { InputNumber } from "../form/InputNumber.js";
import { InputStock, Stock } from "../form/InputStock.js";
import { InputTaxes } from "../form/InputTaxes.js";
import { InputWithAddon } from "../form/InputWithAddon.js";
-import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
type Entity = TalerMerchantApi.ProductDetail & { product_id: string };
@@ -84,11 +82,11 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
}
}
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
);
const submit = useCallback((): Entity | undefined => {
- const stock: Stock = (value as any).stock;
+ const stock = value.stock;
if (!stock) {
value.total_stock = -1;
@@ -101,7 +99,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
: stock.nextRestock;
value.address = stock.address;
}
- delete (value as any).stock;
+ delete value.stock;
if (typeof value.minimum_age !== "undefined" && value.minimum_age < 1) {
delete value.minimum_age;
@@ -116,11 +114,8 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
onSubscribe(hasErrors ? undefined : submit);
}, [submit, hasErrors]);
- const {
- state: { backendUrl },
- } = useSessionContext();
const { i18n } = useTranslationContext();
-
+ const { state } = useSessionContext();
return (
<div>
<FormProvider<Entity>
@@ -132,7 +127,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
{alreadyExist ? undefined : (
<InputWithAddon<Entity>
name="product_id"
- addonBefore={new URL("product/", backendUrl).href}
+ addonBefore={new URL("product/", state.backendUrl.href).href}
label={i18n.str`ID`}
tooltip={i18n.str`product identification to use in URLs (for internal use only)`}
/>
diff --git a/packages/merchant-backoffice-ui/src/context/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts
index 9d63d8e33..fa5e14ab3 100644
--- a/packages/merchant-backoffice-ui/src/context/session.ts
+++ b/packages/merchant-backoffice-ui/src/context/session.ts
@@ -17,11 +17,10 @@
import {
AccessToken,
Codec,
+ TalerMerchantApi,
buildCodecForObject,
- buildCodecForUnion,
- codecForBoolean,
- codecForConstString,
codecForString,
+ codecForURL,
codecOptional,
} from "@gnu-taler/taler-util";
import {
@@ -29,104 +28,79 @@ import {
useLocalStorage,
useMerchantApiContext,
} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, VNode, createContext, h } from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
import { mutate } from "swr";
+import { MerchantLib } from "../../../web-util/lib/context/activity.js";
/**
* Has the information to reach and
* authenticate at the bank's backend.
*/
-export type SessionState = LoggedIn | LoggedOut | Expired;
+export type SessionState = LoggedIn | LoggedOut;
interface LoggedIn {
status: "loggedIn";
- backendUrl: string;
+
+ // is this instance admin? usually "default" name
isAdmin: boolean;
+
+ // url where all the request will be made
+ // usually this is from where the SPA was loaded
+ // unless it's using impersonate feature
+ backendUrl: URL;
+
+ // instance name
instance: string;
+
+ // session is not the same from where it was loaded
+ impersonated: boolean;
+
+ //instance access token
token: AccessToken | undefined;
- impersonate: Impersonate | undefined;
-}
-interface Impersonate {
- originalInstance: string;
- originalToken: AccessToken | undefined;
- originalBackendUrl: string;
-}
-interface Expired {
- status: "expired";
- backendUrl: string;
- isAdmin: boolean;
- instance: string;
- impersonate: Impersonate | undefined;
}
+
interface LoggedOut {
status: "loggedOut";
- backendUrl: string;
+ backendUrl: URL;
instance: string;
isAdmin: boolean;
+ token: AccessToken | undefined;
}
-export const codecForSessionStateLoggedIn = (): Codec<LoggedIn> =>
- buildCodecForObject<LoggedIn>()
- .property("status", codecForConstString("loggedIn"))
- .property("backendUrl", codecForString())
- .property("instance", codecForString())
- .property("impersonate", codecOptional(codecForImpresonate()))
+interface SavedSession {
+ backendUrl: URL;
+ token: AccessToken | undefined;
+ prevToken: AccessToken | undefined;
+}
+
+export const codecForSessionState = (): Codec<SavedSession> =>
+ buildCodecForObject<SavedSession>()
+ .property("backendUrl", codecForURL())
.property("token", codecOptional(codecForString() as Codec<AccessToken>))
- .property("isAdmin", codecForBoolean())
- .build("SessionState.LoggedIn");
-
-export const codecForSessionStateExpired = (): Codec<Expired> =>
- buildCodecForObject<Expired>()
- .property("status", codecForConstString("expired"))
- .property("backendUrl", codecForString())
- .property("instance", codecForString())
- .property("impersonate", codecOptional(codecForImpresonate()))
- .property("isAdmin", codecForBoolean())
- .build("SessionState.Expired");
-
-export const codecForSessionStateLoggedOut = (): Codec<LoggedOut> =>
- buildCodecForObject<LoggedOut>()
- .property("status", codecForConstString("loggedOut"))
- .property("backendUrl", codecForString())
- .property("instance", codecForString())
- .property("isAdmin", codecForBoolean())
- .build("SessionState.LoggedOut");
-
-export const codecForImpresonate = (): Codec<Impersonate> =>
- buildCodecForObject<Impersonate>()
- .property("originalInstance", codecForString())
.property(
- "originalToken",
+ "prevToken",
codecOptional(codecForString() as Codec<AccessToken>),
)
- .property("originalBackendUrl", codecForString())
- .build("SessionState.Impersonate");
-
-export const codecForSessionState = (): Codec<SessionState> =>
- buildCodecForUnion<SessionState>()
- .discriminateOn("status")
- .alternative("loggedIn", codecForSessionStateLoggedIn())
- .alternative("loggedOut", codecForSessionStateLoggedOut())
- .alternative("expired", codecForSessionStateExpired())
- .build("SessionState");
+ .build("SavedSession");
function inferInstanceName(url: URL) {
const match = INSTANCE_ID_LOOKUP.exec(url.href);
return !match || !match[1] ? DEFAULT_ADMIN_USERNAME : match[1];
}
-export const defaultState = (url: URL): SessionState => {
- const instance = inferInstanceName(url);
+export const defaultState = (url: URL): SavedSession => {
return {
- status: "loggedIn",
- instance,
- backendUrl: url.href,
- isAdmin: instance === DEFAULT_ADMIN_USERNAME,
+ backendUrl: url,
token: undefined,
- impersonate: undefined,
+ prevToken: undefined,
};
};
export interface SessionStateHandler {
+ lib: MerchantLib;
+ config: TalerMerchantApi.VersionResponse;
+
state: SessionState;
/**
* from every state to logout state
@@ -137,19 +111,15 @@ export interface SessionStateHandler {
*/
deImpersonate(): void;
/**
- * from non-loggedOut state to expired
- */
- expired(): void;
- /**
* from any to loggedIn
* @param info
*/
- logIn(info: { token?: AccessToken }): void;
+ logIn(token: AccessToken | undefined): void;
/**
* from loggedIn to impersonate
* @param info
*/
- impersonate(info: { instance: string; token?: AccessToken }): void;
+ impersonate(baseUrl: URL): void;
}
const SESSION_STATE_KEY = buildStorageKey(
@@ -161,95 +131,125 @@ export const DEFAULT_ADMIN_USERNAME = "default";
export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/;
+export function cleanAllCache(): void {
+ mutate(() => true, undefined, { revalidate: false });
+}
+
+const Context = createContext<SessionStateHandler>(undefined!);
+
+export const useSessionContext = (): SessionStateHandler => useContext(Context);
+
/**
- * Return getters and setters for
- * login credentials and backend's
- * base URL.
+ * Creates the session in loggedIn state.
+ * Infer the instance name based on the URL.
+ * Create the instance of the merchant api http rest.
+ * Returns API that handle impersonation.
+ *
+ * @param param0
+ * @returns
*/
-export function useSessionContext(): SessionStateHandler {
- const { url } = useMerchantApiContext();
-
+export const SessionContextProvider = ({
+ children,
+ // value,
+}: {
+ // value: MerchantUiSettings;
+ children: ComponentChildren;
+}): VNode => {
+ const {
+ lib: rootLib,
+ config: rootConfig,
+ url: merchantUrl,
+ } = useMerchantApiContext();
+ const [status, setStatus] = useState<"loggedIn" | "loggedOut">("loggedIn");
+ const [currentConfig, setCurrentConfig] =
+ useState<TalerMerchantApi.VersionResponse>();
const { value: state, update } = useLocalStorage(
SESSION_STATE_KEY,
- defaultState(url),
+ defaultState(merchantUrl),
);
- return {
- state,
+ const currentInstance = inferInstanceName(state.backendUrl);
+
+ let lib: MerchantLib;
+ let config: TalerMerchantApi.VersionResponse;
+ const doingImpersonation = state.backendUrl.href !== merchantUrl.href;
+ if (doingImpersonation) {
+ /**
+ * FIXME: can't impersonate other than local instances
+ */
+ lib = rootLib.subInstanceApi(inferInstanceName(state.backendUrl));
+
+ config = currentConfig ?? rootConfig;
+ } else {
+ lib = rootLib;
+ config = rootConfig;
+ }
+
+ useEffect(() => {
+ // FIXME: handle what happen if the subinstance /config
+ // fails
+ if (!doingImpersonation) return;
+ lib.instance.getConfig().then((resp) => {
+ if (resp.type === "ok") {
+ setCurrentConfig(resp.body);
+ }
+ });
+ }, [state.backendUrl.href]);
+
+ const value: SessionStateHandler = {
+ state: {
+ backendUrl: state.backendUrl,
+ token: state.token,
+ impersonated: doingImpersonation,
+ instance: currentInstance,
+ isAdmin: currentInstance === DEFAULT_ADMIN_USERNAME,
+ status: status,
+ },
+ lib,
+ config,
logOut() {
- const instance = inferInstanceName(url);
- const nextState: SessionState = {
- status: "loggedOut",
- backendUrl: url.href,
- instance,
- isAdmin: instance === DEFAULT_ADMIN_USERNAME,
- };
- update(nextState);
+ setStatus("loggedOut");
+ update({
+ backendUrl: merchantUrl,
+ token: undefined,
+ prevToken: undefined,
+ });
+ cleanAllCache();
},
deImpersonate() {
- if (state.status === "loggedOut" || state.status === "expired") {
- // can't impersonate if not loggedin
- return;
- }
- if (state.impersonate === undefined) {
- return;
- }
- const nextState: SessionState = {
- status: "loggedIn",
- backendUrl: state.impersonate.originalBackendUrl,
- isAdmin: state.impersonate.originalInstance === DEFAULT_ADMIN_USERNAME,
- instance: state.impersonate.originalInstance,
- token: state.impersonate.originalToken,
- impersonate: undefined,
- };
- update(nextState);
- },
- impersonate(info) {
- if (state.status === "loggedOut" || state.status === "expired") {
- // can't impersonate if not loggedin
- return;
- }
- const nextState: SessionState = {
- status: "loggedIn",
- backendUrl: new URL(`instances/${info.instance}`, state.backendUrl)
- .href,
- isAdmin: info.instance === DEFAULT_ADMIN_USERNAME,
- instance: info.instance,
- // FIXME: bank and merchant should have consistent behavior
- token: info.token?.substring("secret-token:".length) as AccessToken,
- impersonate: {
- originalBackendUrl: state.backendUrl,
- originalToken: state.token,
- originalInstance: state.instance,
- },
- };
- update(nextState);
+ cleanAllCache();
+ update({
+ backendUrl: merchantUrl,
+ token: state.prevToken,
+ prevToken: undefined,
+ });
+ setStatus("loggedIn");
},
- expired() {
- if (state.status === "loggedOut") return;
-
- const nextState: SessionState = {
- ...state,
- status: "expired",
- };
- update(nextState);
+ impersonate(baseUrl) {
+ /**
+ * FIXME: can't impersonate other than local instances
+ */
+ update({
+ backendUrl: baseUrl,
+ token: undefined,
+ prevToken: state.token,
+ });
+ setStatus("loggedIn");
+ cleanAllCache();
},
- logIn(info) {
- // admin is defined by the username
- const nextState: SessionState = {
- impersonate: undefined,
- ...state,
- status: "loggedIn",
- // FIXME: bank and merchant should have consistent behavior
- token: info.token?.substring("secret-token:".length) as AccessToken,
- // token: info.token,
- };
- update(nextState);
+ logIn(token) {
cleanAllCache();
+ setStatus("loggedIn");
+ update({
+ backendUrl: state.backendUrl,
+ token: token,
+ prevToken: state.prevToken,
+ });
},
};
-}
-function cleanAllCache(): void {
- mutate(() => true, undefined, { revalidate: false });
-}
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts
deleted file mode 100644
index e4e50c8ad..000000000
--- a/packages/merchant-backoffice-ui/src/hooks/backend.ts
+++ /dev/null
@@ -1,375 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import {
- TalerErrorDetail,
- TalerMerchantApi
-} from "@gnu-taler/taler-util";
-import {
- EmptyObject,
- HttpError,
- HttpResponse,
- HttpResponseOk,
- RequestError,
- RequestOptions,
- useApiContext
-} from "@gnu-taler/web-util/browser";
-import { useCallback, useEffect, useState } from "preact/hooks";
-import { useSWRConfig } from "swr";
-import { useSessionContext } from "../context/session.js";
-
-export function useMatchMutate(): (
- re?: RegExp,
- value?: unknown,
-) => Promise<any> {
- const { cache, mutate } = useSWRConfig();
-
- if (!(cache instanceof Map)) {
- throw new Error(
- "matchMutate requires the cache provider to be a Map instance",
- );
- }
-
- return function matchRegexMutate(re?: RegExp) {
- return mutate(
- (key) => {
- // evict if no key or regex === all
- if (!key || !re) return true;
- // match string
- if (typeof key === "string" && re.test(key)) return true;
- // record or object have the path at [0]
- if (typeof key === "object" && re.test(key[0])) return true;
- //key didn't match regex
- return false;
- },
- undefined,
- {
- revalidate: true,
- },
- );
- };
-}
-
-export function useBackendInstancesTestForAdmin(): HttpResponse<
- TalerMerchantApi.InstancesResponse,
- TalerErrorDetail
-> {
- const { request } = useBackendBaseRequest();
-
- type Type = TalerMerchantApi.InstancesResponse;
-
- const [result, setResult] = useState<
- HttpResponse<Type, TalerErrorDetail>
- >({ loading: true });
-
- useEffect(() => {
- request<Type>(`/management/instances`)
- .then((data) => setResult(data))
- .catch((error: RequestError<TalerErrorDetail>) =>
- setResult(error.cause),
- );
- }, [request]);
-
- return result;
-}
-
-const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
-const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
-
-export function useBackendConfig(): HttpResponse<
- TalerMerchantApi.VersionResponse | undefined,
- RequestError<TalerErrorDetail>
-> {
- const { request } = useBackendBaseRequest();
-
- type Type = TalerMerchantApi.VersionResponse;
- type State = {
- data: HttpResponse<Type, RequestError<TalerErrorDetail>>;
- timer: number;
- };
- const [result, setResult] = useState<State>({
- data: { loading: true },
- timer: 0,
- });
-
- useEffect(() => {
- if (result.timer) {
- clearTimeout(result.timer);
- }
- function tryConfig(): void {
- request<Type>(`/config`)
- .then((data) => {
- const timer: any = setTimeout(() => {
- tryConfig();
- }, CHECK_CONFIG_INTERVAL_OK);
- setResult({ data, timer });
- })
- .catch((error) => {
- const timer: any = setTimeout(() => {
- tryConfig();
- }, CHECK_CONFIG_INTERVAL_FAIL);
- const data = error.cause;
- setResult({ data, timer });
- });
- }
- tryConfig();
- }, [request]);
-
- return result.data;
-}
-
-interface useBackendInstanceRequestType {
- request: <T>(
- endpoint: string,
- options?: RequestOptions,
- ) => Promise<HttpResponseOk<T>>;
- fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
- multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>;
- orderFetcher: <T>(
- params: [
- endpoint: string,
- paid?: YesOrNo,
- refunded?: YesOrNo,
- wired?: YesOrNo,
- searchDate?: Date,
- delta?: number,
- ],
- ) => Promise<HttpResponseOk<T>>;
- transferFetcher: <T>(
- params: [
- endpoint: string,
- payto_uri?: string,
- verified?: string,
- position?: string,
- delta?: number,
- ],
- ) => Promise<HttpResponseOk<T>>;
- templateFetcher: <T>(
- params: [endpoint: string, position?: string, delta?: number],
- ) => Promise<HttpResponseOk<T>>;
- webhookFetcher: <T>(
- params: [endpoint: string, position?: string, delta?: number],
- ) => Promise<HttpResponseOk<T>>;
-}
-interface useBackendBaseRequestType {
- request: <T>(
- endpoint: string,
- options?: RequestOptions,
- ) => Promise<HttpResponseOk<T>>;
-}
-
-type YesOrNo = "yes" | "no";
-
-/**
- *
- * @param root the request is intended to the base URL and no the instance URL
- * @returns request handler to
- */
-export function useBackendBaseRequest(): useBackendBaseRequestType {
- const { request: requestHandler } = useApiContext();
- const { state } = useSessionContext();
- const token = state.status === "loggedIn" ? state.token : undefined;
- const baseUrl = state.backendUrl;
-
- const request = useCallback(
- function requestImpl<T>(
- endpoint: string,
- options: RequestOptions = {},
- ): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, { ...options, token })
- .then((res) => {
- return res;
- })
- .catch((err) => {
- throw err;
- });
- },
- [baseUrl, token],
- );
-
- return { request };
-}
-
-export function useBackendInstanceRequest(): useBackendInstanceRequestType {
- const { request: requestHandler } = useApiContext();
-
- const { state } = useSessionContext();
- const token = state.status === "loggedIn" ? state.token : undefined;
- const baseUrl = state.backendUrl;
-
- const request = useCallback(
- function requestImpl<T>(
- endpoint: string,
- options: RequestOptions = {},
- ): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, { token, ...options });
- },
- [baseUrl, token],
- );
-
- const multiFetcher = useCallback(
- function multiFetcherImpl<T>(
- args: [endpoints: string[]],
- ): Promise<HttpResponseOk<T>[]> {
- const [endpoints] = args;
- return Promise.all(
- endpoints.map((endpoint) =>
- requestHandler<T>(baseUrl, endpoint, { token }),
- ),
- );
- },
- [baseUrl, token],
- );
-
- const fetcher = useCallback(
- function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(baseUrl, endpoint, { token });
- },
- [baseUrl, token],
- );
-
- const orderFetcher = useCallback(
- function orderFetcherImpl<T>(
- args: [
- endpoint: string,
- paid?: YesOrNo,
- refunded?: YesOrNo,
- wired?: YesOrNo,
- searchDate?: Date,
- delta?: number,
- ],
- ): Promise<HttpResponseOk<T>> {
- const [endpoint, paid, refunded, wired, searchDate, delta] = args;
- const date_s =
- delta && delta < 0 && searchDate
- ? Math.floor(searchDate.getTime() / 1000) + 1
- : searchDate !== undefined
- ? Math.floor(searchDate.getTime() / 1000)
- : undefined;
- const params: any = {};
- if (paid !== undefined) params.paid = paid;
- if (delta !== undefined) params.delta = delta;
- if (refunded !== undefined) params.refunded = refunded;
- if (wired !== undefined) params.wired = wired;
- if (date_s !== undefined) params.date_s = date_s;
- if (delta === 0) {
- //in this case we can already assume the response
- //and avoid network
- return Promise.resolve({
- ok: true,
- data: { orders: [] } as T,
- });
- }
- return requestHandler<T>(baseUrl, endpoint, { params, token });
- },
- [baseUrl, token],
- );
-
- const transferFetcher = useCallback(
- function transferFetcherImpl<T>(
- args: [
- endpoint: string,
- payto_uri?: string,
- verified?: string,
- position?: string,
- delta?: number,
- ],
- ): Promise<HttpResponseOk<T>> {
- const [endpoint, payto_uri, verified, position, delta] = args;
- const params: any = {};
- if (payto_uri !== undefined) params.payto_uri = payto_uri;
- if (verified !== undefined) params.verified = verified;
- if (delta === 0) {
- //in this case we can already assume the response
- //and avoid network
- return Promise.resolve({
- ok: true,
- data: { transfers: [] } as T,
- });
- }
- if (delta !== undefined) {
- params.limit = delta;
- }
- if (position !== undefined) params.offset = position;
-
- return requestHandler<T>(baseUrl, endpoint, { params, token });
- },
- [baseUrl, token],
- );
-
- const templateFetcher = useCallback(
- function templateFetcherImpl<T>(
- args: [endpoint: string, position?: string, delta?: number],
- ): Promise<HttpResponseOk<T>> {
- const [endpoint, position, delta] = args;
- const params: any = {};
- if (delta === 0) {
- //in this case we can already assume the response
- //and avoid network
- return Promise.resolve({
- ok: true,
- data: { templates: [] } as T,
- });
- }
- if (delta !== undefined) {
- params.limit = delta;
- }
- if (position !== undefined) params.offset = position;
-
- return requestHandler<T>(baseUrl, endpoint, { params, token });
- },
- [baseUrl, token],
- );
-
- const webhookFetcher = useCallback(
- function webhookFetcherImpl<T>(
- args: [endpoint: string, position?: string, delta?: number],
- ): Promise<HttpResponseOk<T>> {
- const [endpoint, position, delta] = args;
- const params: any = {};
- if (delta === 0) {
- //in this case we can already assume the response
- //and avoid network
- return Promise.resolve({
- ok: true,
- data: { webhooks: [] } as T,
- });
- }
- if (delta !== undefined) {
- params.limit = delta;
- }
- if (position !== undefined) params.offset = position;
-
- return requestHandler<T>(baseUrl, endpoint, { params, token });
- },
- [baseUrl, token],
- );
-
- return {
- request,
- fetcher,
- multiFetcher,
- orderFetcher,
- transferFetcher,
- templateFetcher,
- webhookFetcher,
- };
-}
diff --git a/packages/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts
index 3cf9c7846..8857ad839 100644
--- a/packages/merchant-backoffice-ui/src/hooks/bank.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/bank.ts
@@ -14,198 +14,73 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
- HttpResponse,
- HttpResponseOk,
- HttpResponsePaginated,
- RequestError,
+ useMerchantApiContext
} from "@gnu-taler/web-util/browser";
-import { useEffect, useState } from "preact/hooks";
-import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
-import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
import _useSWR, { SWRHook, mutate } from "swr";
-import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useSessionContext } from "../context/session.js";
const useSWR = _useSWR as unknown as SWRHook;
-// const MOCKED_ACCOUNTS: Record<string, TalerMerchantApi.AccountAddDetails> = {
-// "hwire1": {
-// h_wire: "hwire1",
-// payto_uri: "payto://fake/iban/123",
-// salt: "qwe",
-// },
-// "hwire2": {
-// h_wire: "hwire2",
-// payto_uri: "payto://fake/iban/123",
-// salt: "qwe2",
-// },
-// }
-
-export function useBankAccountAPI(): BankAccountAPI {
- const mutateAll = useMatchMutate();
- const { request } = useBackendInstanceRequest();
-
- const createBankAccount = async (
- data: TalerMerchantApi.AccountAddDetails,
- ): Promise<HttpResponseOk<void>> => {
- // MOCKED_ACCOUNTS[data.h_wire] = data
- // return Promise.resolve({ ok: true, data: undefined });
- const res = await request<void>(`/private/accounts`, {
- method: "POST",
- data,
- });
- await mutateAll(/.*private\/accounts.*/);
- return res;
- };
-
- const updateBankAccount = async (
- h_wire: string,
- data: TalerMerchantApi.AccountPatchDetails,
- ): Promise<HttpResponseOk<void>> => {
- // MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials
- // MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url
- // return Promise.resolve({ ok: true, data: undefined });
- const res = await request<void>(`/private/accounts/${h_wire}`, {
- method: "PATCH",
- data,
- });
- await mutateAll(/.*private\/accounts.*/);
- return res;
- };
-
- const deleteBankAccount = async (
- h_wire: string,
- ): Promise<HttpResponseOk<void>> => {
- // delete MOCKED_ACCOUNTS[h_wire]
- // return Promise.resolve({ ok: true, data: undefined });
- const res = await request<void>(`/private/accounts/${h_wire}`, {
- method: "DELETE",
- });
- await mutateAll(/.*private\/accounts.*/);
- return res;
- };
-
- return {
- createBankAccount,
- updateBankAccount,
- deleteBankAccount,
- };
-}
-
-export interface BankAccountAPI {
- createBankAccount: (
- data: TalerMerchantApi.AccountAddDetails,
- ) => Promise<HttpResponseOk<void>>;
- updateBankAccount: (
- id: string,
- data: TalerMerchantApi.AccountPatchDetails,
- ) => Promise<HttpResponseOk<void>>;
- deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>;
-}
-
export interface InstanceBankAccountFilter {
}
export function revalidateInstanceBankAccounts() {
- // mutate(key => key instanceof)
- return mutate((key) => Array.isArray(key) && key[key.length - 1] === "/private/accounts", undefined, { revalidate: true });
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listBankAccounts",
+ undefined,
+ { revalidate: true },
+ );
}
-export function useInstanceBankAccounts(
- args?: InstanceBankAccountFilter,
- updatePosition?: (id: string) => void,
-): HttpResponsePaginated<
- TalerMerchantApi.AccountsSummaryResponse,
- TalerErrorDetail
-> {
-
- const { fetcher } = useBackendInstanceRequest();
+export function useInstanceBankAccounts() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
- const [pageAfter, setPageAfter] = useState(1);
+ // const [offset, setOffset] = useState<string | undefined>();
- const totalAfter = pageAfter * PAGE_SIZE;
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<
- HttpResponseOk<TalerMerchantApi.AccountsSummaryResponse>,
- RequestError<TalerErrorDetail>
- >([`/private/accounts`], fetcher);
-
- const [lastAfter, setLastAfter] = useState<
- HttpResponse<
- TalerMerchantApi.AccountsSummaryResponse,
- TalerErrorDetail
- >
- >({ loading: true });
- useEffect(() => {
- if (afterData) setLastAfter(afterData);
- }, [afterData /*, beforeData*/]);
+ async function fetcher([token, _bid]: [AccessToken, string]) {
+ return await instance.listBankAccounts(token, {
+ // limit: PAGINATED_LIST_REQUEST,
+ // offset: bid,
+ // order: "dec",
+ });
+ }
- if (afterError) return afterError.cause;
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listBankAccounts">,
+ TalerHttpError
+ >([session.token, "offset", "listBankAccounts"], fetcher);
- // if the query returns less that we ask, then we have reach the end or beginning
- const isReachingEnd =
- afterData && afterData.data.accounts.length < totalAfter;
- const isReachingStart = false;
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
- const pagination = {
- isReachingEnd,
- isReachingStart,
- loadMore: () => {
- if (!afterData || isReachingEnd) return;
- if (afterData.data.accounts.length < MAX_RESULT_SIZE) {
- setPageAfter(pageAfter + 1);
- } else {
- const from = `${afterData.data.accounts[afterData.data.accounts.length - 1]
- .h_wire
- }`;
- if (from && updatePosition) updatePosition(from);
- }
- },
- loadMorePrev: () => {
- },
- };
+ // return buildPaginatedResult(data.body.accounts, offset, setOffset, (d) => d.h_wire)
+ return data;
+}
- const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts;
- if (loadingAfter /* || loadingBefore */)
- return { loading: true, data: { accounts } };
- if (/*beforeData &&*/ afterData) {
- return { ok: true, data: { accounts }, ...pagination };
- }
- return { loading: true };
+export function revalidateBankAccountDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getBankAccountDetails",
+ undefined,
+ { revalidate: true },
+ );
}
+export function useBankAccountDetails(h_wire: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
-export function useBankAccountDetails(
- h_wire: string,
-): HttpResponse<
- TalerMerchantApi.BankAccountEntry,
- TalerErrorDetail
-> {
- // return {
- // ok: true,
- // data: {
- // ...MOCKED_ACCOUNTS[h_wire],
- // active: true,
- // }
- // }
- const { fetcher } = useBackendInstanceRequest();
+ async function fetcher([token, wireId]: [AccessToken, string]) {
+ return await instance.getBankAccountDetails(token, wireId);
+ }
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<TalerMerchantApi.BankAccountEntry>,
- RequestError<TalerErrorDetail>
- >([`/private/accounts/${h_wire}`], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getBankAccountDetails">,
+ TalerHttpError
+ >([session.token, h_wire, "getBankAccountDetails"], fetcher);
- if (isValidating) return { loading: true, data: data?.data };
- if (data) {
- return data;
- }
- if (error) return error.cause;
- return { loading: true };
+ if (data) return data;
+ if (error) return error;
+ return undefined;
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
index 3b02d7758..f409592b0 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
@@ -20,6 +20,7 @@
*/
import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
import * as tests from "@gnu-taler/web-util/testing";
import { expect } from "chai";
import {
@@ -36,7 +37,6 @@ import {
API_UPDATE_CURRENT_INSTANCE_AUTH,
API_UPDATE_INSTANCE_BY_ID,
} from "./urls.js";
-import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
describe("instance api interaction with details", () => {
it("should evict cache when updating an instance", async () => {
@@ -58,19 +58,19 @@ describe("instance api interaction with details", () => {
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- name: "instance_name",
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // name: "instance_name",
+ // });
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, {
request: {
name: "other_name",
@@ -81,7 +81,7 @@ describe("instance api interaction with details", () => {
name: "other_name",
} as TalerMerchantApi.QueryInstancesResponse,
});
- api.management.updateCurrentInstance(undefined, {
+ api.instance.updateCurrentInstance(undefined, {
name: "other_name",
} as TalerMerchantApi.InstanceReconfigurationMessage);
},
@@ -89,12 +89,12 @@ describe("instance api interaction with details", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- name: "other_name",
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // name: "other_name",
+ // });
},
],
env.buildTestingContext(),
@@ -126,21 +126,21 @@ describe("instance api interaction with details", () => {
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- name: "instance_name",
- auth: {
- method: "token",
- },
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // name: "instance_name",
+ // auth: {
+ // method: "token",
+ // },
+ // });
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
request: {
method: "token",
@@ -161,7 +161,7 @@ describe("instance api interaction with details", () => {
response: {
name: "instance_name",
auth: {
- type: "token",
+ method: "token",
// token: "secret",
},
} as TalerMerchantApi.QueryInstancesResponse,
@@ -172,16 +172,16 @@ describe("instance api interaction with details", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- name: "instance_name",
- auth: {
- method: "token",
- // token: "secret",
- },
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // name: "instance_name",
+ // auth: {
+ // method: "token",
+ // // token: "secret",
+ // },
+ // });
},
],
env.buildTestingContext(),
@@ -197,7 +197,7 @@ describe("instance api interaction with details", () => {
response: {
name: "instance_name",
auth: {
- type: "token",
+ method: "token",
// token: "not-secret",
},
} as TalerMerchantApi.QueryInstancesResponse,
@@ -212,22 +212,22 @@ describe("instance api interaction with details", () => {
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- name: "instance_name",
- auth: {
- method: "token",
- // token: "not-secret",
- },
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // name: "instance_name",
+ // auth: {
+ // method: "token",
+ // // token: "not-secret",
+ // },
+ // });
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
request: {
method: "external",
@@ -237,12 +237,12 @@ describe("instance api interaction with details", () => {
response: {
name: "instance_name",
auth: {
- type: "external",
+ method: "external",
},
} as TalerMerchantApi.QueryInstancesResponse,
});
- api.management.updateCurrentInstanceAuthentication(undefined, {
+ api.instance.updateCurrentInstanceAuthentication(undefined, {
method: "external"
});
},
@@ -250,15 +250,15 @@ describe("instance api interaction with details", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- name: "instance_name",
- auth: {
- method: "external",
- },
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // name: "instance_name",
+ // auth: {
+ // method: "external",
+ // },
+ // });
},
],
env.buildTestingContext(),
@@ -345,22 +345,22 @@ describe("instance admin api interaction with listing", () => {
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- instances: [
- {
- name: "instance_name",
- },
- ],
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // instances: [
+ // {
+ // name: "instance_name",
+ // },
+ // ],
+ // });
env.addRequestExpectation(API_CREATE_INSTANCE, {
request: {
@@ -380,7 +380,7 @@ describe("instance admin api interaction with listing", () => {
},
});
- api.management.createInstance(undefined, {
+ api.instance.createInstance(undefined, {
name: "other_name",
} as TalerMerchantApi.InstanceConfigurationMessage)
},
@@ -388,19 +388,19 @@ describe("instance admin api interaction with listing", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- instances: [
- {
- name: "instance_name",
- },
- {
- name: "other_name",
- },
- ],
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // instances: [
+ // {
+ // name: "instance_name",
+ // },
+ // {
+ // name: "other_name",
+ // },
+ // ],
+ // });
},
],
env.buildTestingContext(),
@@ -436,27 +436,27 @@ describe("instance admin api interaction with listing", () => {
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- instances: [
- {
- id: "default",
- name: "instance_name",
- },
- {
- id: "the_id",
- name: "second_instance",
- },
- ],
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // instances: [
+ // {
+ // id: "default",
+ // name: "instance_name",
+ // },
+ // {
+ // id: "the_id",
+ // name: "second_instance",
+ // },
+ // ],
+ // });
env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {});
env.addRequestExpectation(API_LIST_INSTANCES, {
@@ -470,23 +470,23 @@ describe("instance admin api interaction with listing", () => {
},
});
- api.management.deleteInstance(undefined, "the_id");
+ api.instance.deleteInstance(undefined, "the_id");
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- instances: [
- {
- id: "default",
- name: "instance_name",
- },
- ],
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // instances: [
+ // {
+ // id: "default",
+ // name: "instance_name",
+ // },
+ // ],
+ // });
},
],
env.buildTestingContext(),
@@ -590,27 +590,27 @@ describe("instance admin api interaction with listing", () => {
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- instances: [
- {
- id: "default",
- name: "instance_name",
- },
- {
- id: "the_id",
- name: "second_instance",
- },
- ],
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // instances: [
+ // {
+ // id: "default",
+ // name: "instance_name",
+ // },
+ // {
+ // id: "the_id",
+ // name: "second_instance",
+ // },
+ // ],
+ // });
env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {
qparam: {
@@ -628,23 +628,23 @@ describe("instance admin api interaction with listing", () => {
},
});
- api.management.deleteInstance(undefined, "the_id", { purge: true })
+ api.instance.deleteInstance(undefined, "the_id", { purge: true })
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- instances: [
- {
- id: "default",
- name: "instance_name",
- },
- ],
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // instances: [
+ // {
+ // id: "default",
+ // name: "instance_name",
+ // },
+ // ],
+ // });
},
],
env.buildTestingContext(),
@@ -678,23 +678,23 @@ describe("instance management api interaction with listing", () => {
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- instances: [
- {
- id: "managed",
- name: "instance_name",
- },
- ],
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // instances: [
+ // {
+ // id: "managed",
+ // name: "instance_name",
+ // },
+ // ],
+ // });
env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID("managed"), {
request: {
@@ -712,7 +712,7 @@ describe("instance management api interaction with listing", () => {
},
});
- api.management.updateCurrentInstance(undefined, {
+ api.instance.updateCurrentInstance(undefined, {
name: "other_name",
} as TalerMerchantApi.InstanceConfigurationMessage);
},
@@ -720,17 +720,17 @@ describe("instance management api interaction with listing", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- instances: [
- {
- id: "managed",
- name: "other_name",
- },
- ],
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // instances: [
+ // {
+ // id: "managed",
+ // name: "other_name",
+ // },
+ // ],
+ // });
},
],
env.buildTestingContext(),
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts
index 0ba68250a..f5f8893cd 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -13,125 +13,112 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- HttpResponse,
- HttpResponseOk,
- RequestError
-} from "@gnu-taler/web-util/browser";
-import {
- useBackendBaseRequest,
- useBackendInstanceRequest
-} from "./backend.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
-import _useSWR, { SWRHook } from "swr";
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
const useSWR = _useSWR as unknown as SWRHook;
-export function useInstanceDetails(): HttpResponse<
- TalerMerchantApi.QueryInstancesResponse,
- TalerErrorDetail
-> {
- const { fetcher } = useBackendInstanceRequest();
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<TalerMerchantApi.QueryInstancesResponse>,
- RequestError<TalerErrorDetail>
- >([`/private/`], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- revalidateIfStale: false,
- errorRetryCount: 0,
- errorRetryInterval: 1,
- shouldRetryOnError: false,
- });
-
- if (isValidating) return { loading: true, data: data?.data };
+export function revalidateInstanceDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentInstanceDetails",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceDetails() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ async function fetcher([token]: [AccessToken]) {
+ return await instance.getCurrentInstanceDetails(token);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getCurrentInstanceDetails">,
+ TalerHttpError
+ >([session.token, "getCurrentInstanceDetails"], fetcher);
+
if (data) return data;
- if (error) return error.cause;
- return { loading: true };
+ if (error) return error;
+ return undefined;
}
-type KYCStatus =
- | { type: "ok" }
- | { type: "redirect"; status: TalerMerchantApi.AccountKycRedirects };
+export function revalidateInstanceKYCDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentIntanceKycStatus",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceKYCDetails() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
-export function useInstanceKYCDetails(): HttpResponse<
- KYCStatus,
- TalerErrorDetail
-> {
- const { fetcher } = useBackendInstanceRequest();
+ async function fetcher([token]: [AccessToken]) {
+ return await instance.getCurrentIntanceKycStatus(token, {});
+ }
const { data, error } = useSWR<
- HttpResponseOk<TalerMerchantApi.AccountKycRedirects>,
- RequestError<TalerErrorDetail>
- >([`/private/kyc`], fetcher, {
- refreshInterval: 60 * 1000,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateIfStale: false,
- revalidateOnMount: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- errorRetryCount: 0,
- errorRetryInterval: 1,
- shouldRetryOnError: false,
- });
-
- if (data) {
- if (data.info?.status === 202)
- return { ok: true, data: { type: "redirect", status: data.data } };
- return { ok: true, data: { type: "ok" } };
- }
- if (error) return error.cause;
- return { loading: true };
+ TalerMerchantManagementResultByMethod<"getCurrentIntanceKycStatus">,
+ TalerHttpError
+ >([session.token, "getCurrentIntanceKycStatus"], fetcher);
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+
+
+}
+
+export function revalidateManagedInstanceDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getInstanceDetails",
+ undefined,
+ { revalidate: true },
+ );
}
+export function useManagedInstanceDetails(instanceId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ async function fetcher([token, instanceId]: [AccessToken, string]) {
+ return await instance.getInstanceDetails(token, instanceId);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getInstanceDetails">,
+ TalerHttpError
+ >([session.token, instanceId, "getInstanceDetails"], fetcher);
-export function useManagedInstanceDetails(
- instanceId: string,
-): HttpResponse<
- TalerMerchantApi.QueryInstancesResponse,
- TalerErrorDetail
-> {
- const { request } = useBackendBaseRequest();
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<TalerMerchantApi.QueryInstancesResponse>,
- RequestError<TalerErrorDetail>
- >([`/management/instances/${instanceId}`], request, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- errorRetryCount: 0,
- errorRetryInterval: 1,
- shouldRetryOnError: false,
- });
-
- if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
- if (error) return error.cause;
- return { loading: true };
+ if (error) return error;
+ return undefined;
}
-export function useBackendInstances(): HttpResponse<
- TalerMerchantApi.InstancesResponse,
- TalerErrorDetail
-> {
- const { request } = useBackendBaseRequest();
+export function revalidateBackendInstances() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listInstances",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useBackendInstances() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<TalerMerchantApi.InstancesResponse>,
- RequestError<TalerErrorDetail>
- >(["/management/instances"], request);
+ async function fetcher([token]: [AccessToken]) {
+ return await instance.listInstances(token);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listInstances">,
+ TalerHttpError
+ >([session.token, "listInstances"], fetcher);
- if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
- if (error) return error.cause;
- return { loading: true };
+ if (error) return error;
+ return undefined;
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/merchant.ts b/packages/merchant-backoffice-ui/src/hooks/merchant.ts
deleted file mode 100644
index 47d9e5624..000000000
--- a/packages/merchant-backoffice-ui/src/hooks/merchant.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import {
- HttpResponse,
- HttpResponseOk,
- HttpResponsePaginated,
- RequestError,
-} from "@gnu-taler/web-util/browser";
-import { useEffect, useState } from "preact/hooks";
-import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
-import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
-
-// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
-import _useSWR, { SWRHook, mutate } from "swr";
-const useSWR = _useSWR as unknown as SWRHook;
-
-// const MOCKED_ACCOUNTS: Record<string, TalerMerchantApi.AccountAddDetails> = {
-// "hwire1": {
-// h_wire: "hwire1",
-// payto_uri: "payto://fake/iban/123",
-// salt: "qwe",
-// },
-// "hwire2": {
-// h_wire: "hwire2",
-// payto_uri: "payto://fake/iban/123",
-// salt: "qwe2",
-// },
-// }
-
-export function useBankAccountAPI(): BankAccountAPI {
- const mutateAll = useMatchMutate();
- const { request } = useBackendInstanceRequest();
-
- const createBankAccount = async (
- data: TalerMerchantApi.AccountAddDetails,
- ): Promise<HttpResponseOk<void>> => {
- // MOCKED_ACCOUNTS[data.h_wire] = data
- // return Promise.resolve({ ok: true, data: undefined });
- const res = await request<void>(`/private/accounts`, {
- method: "POST",
- data,
- });
- await mutateAll(/.*private\/accounts.*/);
- return res;
- };
-
- const updateBankAccount = async (
- h_wire: string,
- data: TalerMerchantApi.AccountPatchDetails,
- ): Promise<HttpResponseOk<void>> => {
- // MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials
- // MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url
- // return Promise.resolve({ ok: true, data: undefined });
- const res = await request<void>(`/private/accounts/${h_wire}`, {
- method: "PATCH",
- data,
- });
- await mutateAll(/.*private\/accounts.*/);
- return res;
- };
-
- const deleteBankAccount = async (
- h_wire: string,
- ): Promise<HttpResponseOk<void>> => {
- // delete MOCKED_ACCOUNTS[h_wire]
- // return Promise.resolve({ ok: true, data: undefined });
- const res = await request<void>(`/private/accounts/${h_wire}`, {
- method: "DELETE",
- });
- await mutateAll(/.*private\/accounts.*/);
- return res;
- };
-
- return {
- createBankAccount,
- updateBankAccount,
- deleteBankAccount,
- };
-}
-
-export interface BankAccountAPI {
- createBankAccount: (
- data: TalerMerchantApi.AccountAddDetails,
- ) => Promise<HttpResponseOk<void>>;
- updateBankAccount: (
- id: string,
- data: TalerMerchantApi.AccountPatchDetails,
- ) => Promise<HttpResponseOk<void>>;
- deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>;
-}
-
-export interface InstanceBankAccountFilter {
-}
-
-export function revalidateInstanceBankAccounts() {
- // mutate(key => key instanceof)
- return mutate((key) => Array.isArray(key) && key[key.length - 1] === "/private/accounts", undefined, { revalidate: true });
-}
-export function useInstanceBankAccounts(
- args?: InstanceBankAccountFilter,
- updatePosition?: (id: string) => void,
-): HttpResponsePaginated<
- TalerMerchantApi.AccountsSummaryResponse,
- TalerErrorDetail
-> {
-
- const { fetcher } = useBackendInstanceRequest();
-
- const [pageAfter, setPageAfter] = useState(1);
-
- const totalAfter = pageAfter * PAGE_SIZE;
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<
- HttpResponseOk<TalerMerchantApi.AccountsSummaryResponse>,
- RequestError<TalerErrorDetail>
- >([`/private/accounts`], fetcher);
-
- const [lastAfter, setLastAfter] = useState<
- HttpResponse<
- TalerMerchantApi.AccountsSummaryResponse,
- TalerErrorDetail
- >
- >({ loading: true });
- useEffect(() => {
- if (afterData) setLastAfter(afterData);
- }, [afterData /*, beforeData*/]);
-
- if (afterError) return afterError.cause;
-
- // if the query returns less that we ask, then we have reach the end or beginning
- const isReachingEnd =
- afterData && afterData.data.accounts.length < totalAfter;
- const isReachingStart = false;
-
- const pagination = {
- isReachingEnd,
- isReachingStart,
- loadMore: () => {
- if (!afterData || isReachingEnd) return;
- if (afterData.data.accounts.length < MAX_RESULT_SIZE) {
- setPageAfter(pageAfter + 1);
- } else {
- const from = `${afterData.data.accounts[afterData.data.accounts.length - 1]
- .h_wire
- }`;
- if (from && updatePosition) updatePosition(from);
- }
- },
- loadMorePrev: () => {
- },
- };
-
- const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts;
- if (loadingAfter /* || loadingBefore */)
- return { loading: true, data: { accounts } };
- if (/*beforeData &&*/ afterData) {
- return { ok: true, data: { accounts }, ...pagination };
- }
- return { loading: true };
-}
-
-export function useBankAccountDetails(
- h_wire: string,
-): HttpResponse<
- TalerMerchantApi.BankAccountEntry,
- TalerErrorDetail
-> {
- // return {
- // ok: true,
- // data: {
- // ...MOCKED_ACCOUNTS[h_wire],
- // active: true,
- // }
- // }
- const { fetcher } = useBackendInstanceRequest();
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<TalerMerchantApi.BankAccountEntry>,
- RequestError<TalerErrorDetail>
- >([`/private/accounts/${h_wire}`], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- if (isValidating) return { loading: true, data: data?.data };
- if (data) {
- return data;
- }
- if (error) return error.cause;
- return { loading: true };
-}
diff --git a/packages/merchant-backoffice-ui/src/hooks/order.test.ts b/packages/merchant-backoffice-ui/src/hooks/order.test.ts
index 0d4199875..9c1eaccbb 100644
--- a/packages/merchant-backoffice-ui/src/hooks/order.test.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/order.test.ts
@@ -19,10 +19,10 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { AbsoluteTime, AmountString, TalerMerchantApi } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import { expect } from "chai";
-import { useInstanceOrders, useOrderAPI, useOrderDetails } from "./order.js";
+import { useInstanceOrders, useOrderDetails } from "./order.js";
import { ApiMockEnvironment } from "./testing.js";
import {
API_CREATE_ORDER,
@@ -32,6 +32,7 @@ import {
API_LIST_ORDERS,
API_REFUND_ORDER_BY_ID,
} from "./urls.js";
+import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
describe("order api interaction with listing", () => {
it("should evict cache when creating an order", async () => {
@@ -44,31 +45,31 @@ describe("order api interaction with listing", () => {
},
});
- const newDate = (d: Date) => {
+ const newDate = (_d: string | undefined) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
- const query = useInstanceOrders({ paid: "yes" }, newDate);
- const api = useOrderAPI();
+ const query = useInstanceOrders({ paid: true }, newDate);
+ const { lib: api } = useMerchantApiContext()
return { query, api };
},
{},
[
- ({ query, api }) => {
- expect(query.loading).true;
+ ({ query }) => {
+ expect(query).undefined;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- orders: [{ order_id: "1" }, { order_id: "2" }],
- });
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // orders: [{ order_id: "1" }, { order_id: "2" }],
+ // });
env.addRequestExpectation(API_CREATE_ORDER, {
request: {
@@ -85,21 +86,21 @@ describe("order api interaction with listing", () => {
},
});
- api.createOrder({
- order: { amount: "ARS:12", summary: "pay me" },
- } as any);
+ api.instance.createOrder(undefined, {
+ order: { amount: "ARS:12" as AmountString, summary: "pay me" },
+ })
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }],
- });
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }],
+ // });
},
],
env.buildTestingContext(),
@@ -122,38 +123,38 @@ describe("order api interaction with listing", () => {
},
});
- const newDate = (d: Date) => {
+ const newDate = (_d: string | undefined) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
- const query = useInstanceOrders({ paid: "yes" }, newDate);
- const api = useOrderAPI();
+ const query = useInstanceOrders({ paid: true }, newDate);
+ const { lib: api } = useMerchantApiContext()
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- orders: [
- {
- order_id: "1",
- amount: "EUR:12",
- refundable: true,
- },
- ],
- });
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // orders: [
+ // {
+ // order_id: "1",
+ // amount: "EUR:12",
+ // refundable: true,
+ // },
+ // ],
+ // });
env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), {
request: {
reason: "double pay",
@@ -170,28 +171,28 @@ describe("order api interaction with listing", () => {
},
});
- api.refundOrder("1", {
+ api.instance.addRefund(undefined, "1", {
reason: "double pay",
refund: "EUR:1" as AmountString,
- });
+ })
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- orders: [
- {
- order_id: "1",
- amount: "EUR:12",
- refundable: false,
- },
- ],
- });
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // orders: [
+ // {
+ // order_id: "1",
+ // amount: "EUR:12",
+ // refundable: false,
+ // },
+ // ],
+ // });
},
],
env.buildTestingContext(),
@@ -211,31 +212,31 @@ describe("order api interaction with listing", () => {
},
});
- const newDate = (d: Date) => {
+ const newDate = (_d: string | undefined) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
- const query = useInstanceOrders({ paid: "yes" }, newDate);
- const api = useOrderAPI();
+ const query = useInstanceOrders({ paid: true }, newDate);
+ const { lib: api } = useMerchantApiContext()
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- orders: [{ order_id: "1" }, { order_id: "2" }],
- });
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // orders: [{ order_id: "1" }, { order_id: "2" }],
+ // });
env.addRequestExpectation(API_DELETE_ORDER("1"), {});
@@ -246,18 +247,18 @@ describe("order api interaction with listing", () => {
},
});
- api.deleteOrder("1");
+ api.instance.deleteOrder(undefined, "1")
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- orders: [{ order_id: "2" }],
- });
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // orders: [{ order_id: "2" }],
+ // });
},
],
env.buildTestingContext(),
@@ -279,32 +280,28 @@ describe("order api interaction with details", () => {
} as unknown as TalerMerchantApi.CheckPaymentPaidResponse,
});
- const newDate = (d: Date) => {
- //console.log("new date", d);
- };
-
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useOrderDetails("1");
- const api = useOrderAPI();
+ const { lib: api } = useMerchantApiContext()
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- summary: "description",
- refund_amount: "EUR:0",
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // summary: "description",
+ // refund_amount: "EUR:0",
+ // });
env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), {
request: {
reason: "double pay",
@@ -319,22 +316,22 @@ describe("order api interaction with details", () => {
} as unknown as TalerMerchantApi.CheckPaymentPaidResponse,
});
- api.refundOrder("1", {
+ api.instance.addRefund(undefined, "1", {
reason: "double pay",
refund: "EUR:1" as AmountString,
- });
+ })
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- summary: "description",
- refund_amount: "EUR:1",
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // summary: "description",
+ // refund_amount: "EUR:1",
+ // });
},
],
env.buildTestingContext(),
@@ -355,32 +352,28 @@ describe("order api interaction with details", () => {
} as unknown as TalerMerchantApi.CheckPaymentPaidResponse,
});
- const newDate = (d: Date) => {
- //console.log("new date", d);
- };
-
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useOrderDetails("1");
- const api = useOrderAPI();
+ const { lib: api } = useMerchantApiContext()
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- summary: "description",
- refund_amount: "EUR:0",
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // summary: "description",
+ // refund_amount: "EUR:0",
+ // });
env.addRequestExpectation(API_FORGET_ORDER_BY_ID("1"), {
request: {
fields: ["$.summary"],
@@ -393,20 +386,20 @@ describe("order api interaction with details", () => {
} as unknown as TalerMerchantApi.CheckPaymentPaidResponse,
});
- api.forgetOrder("1", {
+ api.instance.forgetOrder(undefined, "1", {
fields: ["$.summary"],
- });
+ })
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- summary: undefined,
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // summary: undefined,
+ // });
},
],
env.buildTestingContext(),
@@ -433,38 +426,35 @@ describe("order listing pagination", () => {
},
});
- const newDate = (d: Date) => {
+ const newDate = (_d: string | undefined) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const date = new Date(12000);
- const query = useInstanceOrders({ wired: "yes", date }, newDate);
- const api = useOrderAPI();
+ const query = useInstanceOrders({ wired: true, date: AbsoluteTime.fromMilliseconds(date.getTime()) }, newDate);
+ const { lib: api } = useMerchantApiContext()
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- orders: [{ order_id: "1" }, { order_id: "2" }],
- });
- expect(query.isReachingEnd).true;
- expect(query.isReachingStart).true;
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // orders: [{ order_id: "1" }, { order_id: "2" }],
+ // });
+ // expect(query.isReachingEnd).true;
+ // expect(query.isReachingStart).true;
- // should not trigger new state update or query
- query.loadMore();
- query.loadMorePrev();
},
],
env.buildTestingContext(),
@@ -474,7 +464,7 @@ describe("order listing pagination", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
- it("should load more if result brings more that PAGE_SIZE", async () => {
+ it("should load more if result brings more that PAGINATED_LIST_REQUEST", async () => {
const env = new ApiMockEnvironment();
const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({
@@ -483,7 +473,6 @@ describe("order listing pagination", () => {
const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({
order_id: String(i + 20),
}));
- const ordersFrom20to0 = [...ordersFrom0to20].reverse();
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 20, wired: "yes", date_s: 12 },
@@ -499,34 +488,34 @@ describe("order listing pagination", () => {
},
});
- const newDate = (d: Date) => {
+ const newDate = (_d: string | undefined) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const date = new Date(12000);
- const query = useInstanceOrders({ wired: "yes", date }, newDate);
- const api = useOrderAPI();
+ const query = useInstanceOrders({ wired: true, date: AbsoluteTime.fromMilliseconds(date.getTime()) }, newDate);
+ const { lib: api } = useMerchantApiContext()
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- orders: [...ordersFrom20to0, ...ordersFrom20to40],
- });
- expect(query.isReachingEnd).false;
- expect(query.isReachingStart).false;
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // orders: [...ordersFrom20to0, ...ordersFrom20to40],
+ // });
+ // expect(query.isReachingEnd).false;
+ // expect(query.isReachingStart).false;
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -40, wired: "yes", date_s: 13 },
@@ -535,25 +524,25 @@ describe("order listing pagination", () => {
},
});
- query.loadMore();
+ // query.loadMore();
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- orders: [
- ...ordersFrom20to0,
- ...ordersFrom20to40,
- { order_id: "41" },
- ],
- });
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // orders: [
+ // ...ordersFrom20to0,
+ // ...ordersFrom20to40,
+ // { order_id: "41" },
+ // ],
+ // });
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 40, wired: "yes", date_s: 12 },
@@ -562,26 +551,26 @@ describe("order listing pagination", () => {
},
});
- query.loadMorePrev();
+ // query.loadMorePrev();
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- orders: [
- { order_id: "-1" },
- ...ordersFrom20to0,
- ...ordersFrom20to40,
- { order_id: "41" },
- ],
- });
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // orders: [
+ // { order_id: "-1" },
+ // ...ordersFrom20to0,
+ // ...ordersFrom20to40,
+ // { order_id: "41" },
+ // ],
+ // });
},
],
env.buildTestingContext(),
diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts
index 39bc1725b..d0513dc40 100644
--- a/packages/merchant-backoffice-ui/src/hooks/order.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/order.ts
@@ -13,277 +13,86 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- HttpResponse,
- HttpResponseOk,
- HttpResponsePaginated,
- RequestError,
-} from "@gnu-taler/web-util/browser";
-import { useEffect, useState } from "preact/hooks";
-import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
-import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
-import _useSWR, { SWRHook } from "swr";
+import { AbsoluteTime, AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+import { buildPaginatedResult } from "./webhooks.js";
const useSWR = _useSWR as unknown as SWRHook;
-export interface OrderAPI {
- //FIXME: add OutOfStockResponse on 410
- createOrder: (
- data: TalerMerchantApi.PostOrderRequest,
- ) => Promise<HttpResponseOk<TalerMerchantApi.PostOrderResponse>>;
- forgetOrder: (
- id: string,
- data: TalerMerchantApi.ForgetRequest,
- ) => Promise<HttpResponseOk<void>>;
- refundOrder: (
- id: string,
- data: TalerMerchantApi.RefundRequest,
- ) => Promise<HttpResponseOk<TalerMerchantApi.MerchantRefundResponse>>;
- deleteOrder: (id: string) => Promise<HttpResponseOk<void>>;
- getPaymentURL: (id: string) => Promise<HttpResponseOk<string>>;
-}
-
-type YesOrNo = "yes" | "no";
-
-export function useOrderAPI(): OrderAPI {
- const mutateAll = useMatchMutate();
- const { request } = useBackendInstanceRequest();
-
- const createOrder = async (
- data: TalerMerchantApi.PostOrderRequest,
- ): Promise<HttpResponseOk<TalerMerchantApi.PostOrderResponse>> => {
- const res = await request<TalerMerchantApi.PostOrderResponse>(
- `/private/orders`,
- {
- method: "POST",
- data,
- },
- );
- await mutateAll(/.*private\/orders.*/);
- // mutate('')
- return res;
- };
- const refundOrder = async (
- orderId: string,
- data: TalerMerchantApi.RefundRequest,
- ): Promise<HttpResponseOk<TalerMerchantApi.MerchantRefundResponse>> => {
- mutateAll(/@"\/private\/orders"@/);
- const res = request<TalerMerchantApi.MerchantRefundResponse>(
- `/private/orders/${orderId}/refund`,
- {
- method: "POST",
- data,
- },
- );
-
- // order list returns refundable information, so we must evict everything
- await mutateAll(/.*private\/orders.*/);
- return res;
- };
-
- const forgetOrder = async (
- orderId: string,
- data: TalerMerchantApi.ForgetRequest,
- ): Promise<HttpResponseOk<void>> => {
- mutateAll(/@"\/private\/orders"@/);
- const res = request<void>(`/private/orders/${orderId}/forget`, {
- method: "PATCH",
- data,
- });
- // we may be forgetting some fields that are pare of the listing, so we must evict everything
- await mutateAll(/.*private\/orders.*/);
- return res;
- };
- const deleteOrder = async (
- orderId: string,
- ): Promise<HttpResponseOk<void>> => {
- mutateAll(/@"\/private\/orders"@/);
- const res = request<void>(`/private/orders/${orderId}`, {
- method: "DELETE",
- });
- await mutateAll(/.*private\/orders.*/);
- return res;
- };
- const getPaymentURL = async (
- orderId: string,
- ): Promise<HttpResponseOk<string>> => {
- return request<TalerMerchantApi.MerchantOrderStatusResponse>(
- `/private/orders/${orderId}`,
- {
- method: "GET",
- },
- ).then((res) => {
- const url =
- res.data.order_status === "unpaid"
- ? res.data.taler_pay_uri
- : res.data.contract_terms.fulfillment_url;
- const response: HttpResponseOk<string> = res as any;
- response.data = url || "";
- return response;
- });
- };
- return { createOrder, forgetOrder, deleteOrder, refundOrder, getPaymentURL };
+export function revalidateOrderDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getOrderDetails",
+ undefined,
+ { revalidate: true },
+ );
}
+export function useOrderDetails(oderId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
-export function useOrderDetails(
- oderId: string,
-): HttpResponse<
- TalerMerchantApi.MerchantOrderStatusResponse,
- TalerErrorDetail
-> {
- const { fetcher } = useBackendInstanceRequest();
+ async function fetcher([dId, token]: [string, AccessToken]) {
+ return await instance.getOrderDetails(token, dId);
+ }
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<TalerMerchantApi.MerchantOrderStatusResponse>,
- RequestError<TalerErrorDetail>
- >([`/private/orders/${oderId}`], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getOrderDetails">,
+ TalerHttpError
+ >([oderId, session.token, "getOrderDetails"], fetcher);
- if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
- if (error) return error.cause;
- return { loading: true };
+ if (error) return error;
+ return undefined;
}
export interface InstanceOrderFilter {
- paid?: YesOrNo;
- refunded?: YesOrNo;
- wired?: YesOrNo;
- date?: Date;
+ paid?: boolean;
+ refunded?: boolean;
+ wired?: boolean;
+ date?: AbsoluteTime;
+ position?: string;
}
+export function revalidateInstanceOrders() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listOrders",
+ undefined,
+ { revalidate: true },
+ );
+}
export function useInstanceOrders(
args?: InstanceOrderFilter,
- updateFilter?: (d: Date) => void,
-): HttpResponsePaginated<
- TalerMerchantApi.OrderHistory,
- TalerErrorDetail
-> {
- const { orderFetcher } = useBackendInstanceRequest();
-
- const [pageBefore, setPageBefore] = useState(1);
- const [pageAfter, setPageAfter] = useState(1);
-
- const totalAfter = pageAfter * PAGE_SIZE;
- const totalBefore = args?.date ? pageBefore * PAGE_SIZE : 0;
-
- /**
- * FIXME: this can be cleaned up a little
- *
- * the logic of double query should be inside the orderFetch so from the hook perspective and cache
- * is just one query and one error status
- */
- const {
- data: beforeData,
- error: beforeError,
- isValidating: loadingBefore,
- } = useSWR<
- HttpResponseOk<TalerMerchantApi.OrderHistory>,
- RequestError<TalerErrorDetail>
- >(
- [
- `/private/orders`,
- args?.paid,
- args?.refunded,
- args?.wired,
- args?.date,
- totalBefore,
- ],
- orderFetcher,
- );
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<
- HttpResponseOk<TalerMerchantApi.OrderHistory>,
- RequestError<TalerErrorDetail>
- >(
- [
- `/private/orders`,
- args?.paid,
- args?.refunded,
- args?.wired,
- args?.date,
- -totalAfter,
- ],
- orderFetcher,
- );
-
- //this will save last result
- const [lastBefore, setLastBefore] = useState<
- HttpResponse<
- TalerMerchantApi.OrderHistory,
- TalerErrorDetail
- >
- >({ loading: true });
- const [lastAfter, setLastAfter] = useState<
- HttpResponse<
- TalerMerchantApi.OrderHistory,
- TalerErrorDetail
- >
- >({ loading: true });
- useEffect(() => {
- if (afterData) setLastAfter(afterData);
- if (beforeData) setLastBefore(beforeData);
- }, [afterData, beforeData]);
-
- if (beforeError) return beforeError.cause;
- if (afterError) return afterError.cause;
+ updatePosition: (d: string | undefined) => void = () => { },
+) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ // const [offset, setOffset] = useState<string | undefined>(args?.position);
+
+ async function fetcher([token, o, p, r, w, d]: [AccessToken, string, boolean, boolean, boolean, AbsoluteTime]) {
+ return await instance.listOrders(token, {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: o,
+ order: "dec",
+ paid: p,
+ refunded: r,
+ wired: w,
+ date: d,
+ });
+ }
- // if the query returns less that we ask, then we have reach the end or beginning
- const isReachingEnd = afterData && afterData.data.orders.length < totalAfter;
- const isReachingStart =
- args?.date === undefined ||
- (beforeData && beforeData.data.orders.length < totalBefore);
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listOrders">,
+ TalerHttpError
+ >([session.token, args?.position, args?.paid, args?.refunded, args?.wired, args?.date, "listOrders"], fetcher);
- const pagination = {
- isReachingEnd,
- isReachingStart,
- loadMore: () => {
- if (!afterData || isReachingEnd) return;
- if (afterData.data.orders.length < MAX_RESULT_SIZE) {
- setPageAfter(pageAfter + 1);
- } else {
- const from =
- afterData.data.orders[afterData.data.orders.length - 1].timestamp.t_s;
- if (from && from !== "never" && updateFilter)
- updateFilter(new Date(from * 1000));
- }
- },
- loadMorePrev: () => {
- if (!beforeData || isReachingStart) return;
- if (beforeData.data.orders.length < MAX_RESULT_SIZE) {
- setPageBefore(pageBefore + 1);
- } else if (beforeData) {
- const from =
- beforeData.data.orders[beforeData.data.orders.length - 1].timestamp
- .t_s;
- if (from && from !== "never" && updateFilter)
- updateFilter(new Date(from * 1000));
- }
- },
- };
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
- const orders =
- !beforeData || !afterData
- ? []
- : (beforeData || lastBefore).data.orders
- .slice()
- .reverse()
- .concat((afterData || lastAfter).data.orders);
- if (loadingAfter || loadingBefore) return { loading: true, data: { orders } };
- if (beforeData && afterData) {
- return { ok: true, data: { orders }, ...pagination };
- }
- return { loading: true };
+ return buildPaginatedResult(data.body.orders, args?.position, updatePosition, (d) => String(d.row_id))
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts
index 4b45dcf06..41ed89f70 100644
--- a/packages/merchant-backoffice-ui/src/hooks/otp.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts
@@ -13,196 +13,68 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- HttpResponse,
- HttpResponseOk,
- HttpResponsePaginated,
- RequestError,
-} from "@gnu-taler/web-util/browser";
-import { useEffect, useState } from "preact/hooks";
-import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
-import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
-import _useSWR, { SWRHook } from "swr";
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
const useSWR = _useSWR as unknown as SWRHook;
-export function useOtpDeviceAPI(): OtpDeviceAPI {
- const mutateAll = useMatchMutate();
- const { request } = useBackendInstanceRequest();
+export function revalidateInstanceOtpDevices() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listOtpDevices",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceOtpDevices() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
- const createOtpDevice = async (
- data: TalerMerchantApi.OtpDeviceAddDetails,
- ): Promise<HttpResponseOk<void>> => {
- // MOCKED_DEVICES[data.otp_device_id] = data
- // return Promise.resolve({ ok: true, data: undefined });
- const res = await request<void>(`/private/otp-devices`, {
- method: "POST",
- data,
- });
- await mutateAll(/.*private\/otp-devices.*/);
- return res;
- };
+ // const [offset, setOffset] = useState<string | undefined>();
- const updateOtpDevice = async (
- deviceId: string,
- data: TalerMerchantApi.OtpDevicePatchDetails,
- ): Promise<HttpResponseOk<void>> => {
- // MOCKED_DEVICES[deviceId].otp_algorithm = data.otp_algorithm
- // MOCKED_DEVICES[deviceId].otp_ctr = data.otp_ctr
- // MOCKED_DEVICES[deviceId].otp_device_description = data.otp_device_description
- // MOCKED_DEVICES[deviceId].otp_key = data.otp_key
- // return Promise.resolve({ ok: true, data: undefined });
- const res = await request<void>(`/private/otp-devices/${deviceId}`, {
- method: "PATCH",
- data,
+ async function fetcher([token, _bid]: [AccessToken, string]) {
+ return await instance.listOtpDevices(token, {
+ // limit: PAGINATED_LIST_REQUEST,
+ // offset: bid,
+ // order: "dec",
});
- await mutateAll(/.*private\/otp-devices.*/);
- return res;
- };
+ }
- const deleteOtpDevice = async (
- deviceId: string,
- ): Promise<HttpResponseOk<void>> => {
- // delete MOCKED_DEVICES[deviceId]
- // return Promise.resolve({ ok: true, data: undefined });
- const res = await request<void>(`/private/otp-devices/${deviceId}`, {
- method: "DELETE",
- });
- await mutateAll(/.*private\/otp-devices.*/);
- return res;
- };
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listOtpDevices">,
+ TalerHttpError
+ >([session.token, "offset", "listOtpDevices"], fetcher);
- return {
- createOtpDevice,
- updateOtpDevice,
- deleteOtpDevice,
- };
-}
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
-export interface OtpDeviceAPI {
- createOtpDevice: (
- data: TalerMerchantApi.OtpDeviceAddDetails,
- ) => Promise<HttpResponseOk<void>>;
- updateOtpDevice: (
- id: string,
- data: TalerMerchantApi.OtpDevicePatchDetails,
- ) => Promise<HttpResponseOk<void>>;
- deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>;
+ // return buildPaginatedResult(data.body.otp_devices, offset, setOffset, (d) => d.otp_device_id)
+ return data;
}
-export interface InstanceOtpDeviceFilter {
+export function revalidateOtpDeviceDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getOtpDeviceDetails",
+ undefined,
+ { revalidate: true },
+ );
}
+export function useOtpDeviceDetails(deviceId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
-export function useInstanceOtpDevices(
- args?: InstanceOtpDeviceFilter,
- updatePosition?: (id: string) => void,
-): HttpResponsePaginated<
- TalerMerchantApi.OtpDeviceSummaryResponse,
- TalerErrorDetail
-> {
- // return {
- // ok: true,
- // loadMore: () => { },
- // loadMorePrev: () => { },
- // data: {
- // otp_devices: Object.values(MOCKED_DEVICES).map(d => ({
- // device_description: d.otp_device_description,
- // otp_device_id: d.otp_device_id
- // }))
- // }
- // }
-
- const { fetcher } = useBackendInstanceRequest();
-
- const [pageAfter, setPageAfter] = useState(1);
-
- const totalAfter = pageAfter * PAGE_SIZE;
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<
- HttpResponseOk<TalerMerchantApi.OtpDeviceSummaryResponse>,
- RequestError<TalerErrorDetail>
- >([`/private/otp-devices`], fetcher);
-
- const [lastAfter, setLastAfter] = useState<
- HttpResponse<
- TalerMerchantApi.OtpDeviceSummaryResponse,
- TalerErrorDetail
- >
- >({ loading: true });
- useEffect(() => {
- if (afterData) setLastAfter(afterData);
- }, [afterData /*, beforeData*/]);
-
- if (afterError) return afterError.cause;
-
- // if the query returns less that we ask, then we have reach the end or beginning
- const isReachingEnd =
- afterData && afterData.data.otp_devices.length < totalAfter;
- const isReachingStart = true;
-
- const pagination = {
- isReachingEnd,
- isReachingStart,
- loadMore: () => {
- if (!afterData || isReachingEnd) return;
- if (afterData.data.otp_devices.length < MAX_RESULT_SIZE) {
- setPageAfter(pageAfter + 1);
- } else {
- const from = `${afterData.data.otp_devices[afterData.data.otp_devices.length - 1]
- .otp_device_id
- }`;
- if (from && updatePosition) updatePosition(from);
- }
- },
- loadMorePrev: () => {
- },
- };
-
- const otp_devices = !afterData ? [] : (afterData || lastAfter).data.otp_devices;
- if (loadingAfter /* || loadingBefore */)
- return { loading: true, data: { otp_devices } };
- if (/*beforeData &&*/ afterData) {
- return { ok: true, data: { otp_devices }, ...pagination };
+ async function fetcher([dId, token]: [string, AccessToken]) {
+ return await instance.getOtpDeviceDetails(token, dId);
}
- return { loading: true };
-}
-export function useOtpDeviceDetails(
- deviceId: string,
-): HttpResponse<
- TalerMerchantApi.OtpDeviceDetails,
- TalerErrorDetail
-> {
- // return {
- // ok: true,
- // data: {
- // device_description: MOCKED_DEVICES[deviceId].otp_device_description,
- // otp_algorithm: MOCKED_DEVICES[deviceId].otp_algorithm,
- // otp_ctr: MOCKED_DEVICES[deviceId].otp_ctr
- // }
- // }
- const { fetcher } = useBackendInstanceRequest();
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getOtpDeviceDetails">,
+ TalerHttpError
+ >([deviceId, session.token, "getOtpDeviceDetails"], fetcher);
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<TalerMerchantApi.OtpDeviceDetails>,
- RequestError<TalerErrorDetail>
- >([`/private/otp-devices/${deviceId}`], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- if (isValidating) return { loading: true, data: data?.data };
- if (data) {
- return data;
- }
- if (error) return error.cause;
- return { loading: true };
+ if (data) return data;
+ if (error) return error;
+ return undefined;
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts
index 5a50eb378..a21d2921c 100644
--- a/packages/merchant-backoffice-ui/src/hooks/preference.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/preference.ts
@@ -28,12 +28,14 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
export interface Preferences {
advanceOrderMode: boolean;
hideKycUntil: AbsoluteTime;
+ hideMissingAccountUntil: AbsoluteTime;
dateFormat: "ymd" | "dmy" | "mdy";
}
const defaultSettings: Preferences = {
advanceOrderMode: false,
hideKycUntil: AbsoluteTime.never(),
+ hideMissingAccountUntil: AbsoluteTime.never(),
dateFormat: "ymd",
};
@@ -41,6 +43,7 @@ export const codecForPreferences = (): Codec<Preferences> =>
buildCodecForObject<Preferences>()
.property("advanceOrderMode", codecForBoolean())
.property("hideKycUntil", codecForAbsoluteTime)
+ .property("hideMissingAccountUntil", codecForAbsoluteTime)
.property(
"dateFormat",
codecForEither(
diff --git a/packages/merchant-backoffice-ui/src/hooks/product.test.ts b/packages/merchant-backoffice-ui/src/hooks/product.test.ts
index 64dbd0103..39281241c 100644
--- a/packages/merchant-backoffice-ui/src/hooks/product.test.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/product.test.ts
@@ -23,7 +23,6 @@ import * as tests from "@gnu-taler/web-util/testing";
import { expect } from "chai";
import {
useInstanceProducts,
- useProductAPI,
useProductDetails,
} from "./product.js";
import { ApiMockEnvironment } from "./testing.js";
@@ -35,6 +34,7 @@ import {
API_UPDATE_PRODUCT_BY_ID,
} from "./urls.js";
import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
describe("product api interaction with listing", () => {
it("should evict cache when creating a product", async () => {
@@ -52,25 +52,25 @@ describe("product api interaction with listing", () => {
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceProducts();
- const api = useProductAPI();
+ const { lib: api } = useMerchantApiContext();
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals([{ id: "1234" , price: "ARS:12" }]);
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
env.addRequestExpectation(API_CREATE_PRODUCT, {
request: {
@@ -99,7 +99,7 @@ describe("product api interaction with listing", () => {
} as TalerMerchantApi.ProductDetail,
});
- api.createProduct({
+ api.instance.addProduct(undefined, {
price: "ARS:23",
} as any);
},
@@ -107,25 +107,25 @@ describe("product api interaction with listing", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals([
- {
- id: "1234",
- price: "ARS:12",
- },
- {
- id: "2345",
- price: "ARS:23",
- },
- ]);
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals([
+ // {
+ // id: "1234",
+ // price: "ARS:12",
+ // },
+ // {
+ // id: "2345",
+ // price: "ARS:23",
+ // },
+ // ]);
},
],
env.buildTestingContext(),
@@ -150,25 +150,25 @@ describe("product api interaction with listing", () => {
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceProducts();
- const api = useProductAPI();
+ const { lib: api } = useMerchantApiContext();
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), {
request: {
@@ -187,7 +187,7 @@ describe("product api interaction with listing", () => {
} as TalerMerchantApi.ProductDetail,
});
- api.updateProduct("1234", {
+ api.instance.updateProduct(undefined, "1234", {
price: "ARS:13",
} as any);
},
@@ -195,15 +195,15 @@ describe("product api interaction with listing", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals([
- {
- id: "1234",
- price: "ARS:13",
- },
- ]);
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals([
+ // {
+ // id: "1234",
+ // price: "ARS:13",
+ // },
+ // ]);
},
],
env.buildTestingContext(),
@@ -218,7 +218,7 @@ describe("product api interaction with listing", () => {
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
- products: [{ product_id: "1234" , product_serial: 1}, { product_id: "2345", product_serial: 2 }],
+ products: [{ product_id: "1234", product_serial: 1 }, { product_id: "2345", product_serial: 2 }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
@@ -231,28 +231,28 @@ describe("product api interaction with listing", () => {
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceProducts();
- const api = useProductAPI();
+ const { lib: api } = useMerchantApiContext();
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals([
- { id: "1234", price: "ARS:12" },
- { id: "2345", price: "ARS:23" },
- ]);
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals([
+ // { id: "1234", price: "ARS:12" },
+ // { id: "2345", price: "ARS:23" },
+ // ]);
env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {});
@@ -267,22 +267,22 @@ describe("product api interaction with listing", () => {
price: "ARS:12",
} as TalerMerchantApi.ProductDetail,
});
- api.deleteProduct("2345");
+ api.instance.deleteProduct(undefined, "2345");
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]);
},
],
env.buildTestingContext(),
@@ -306,24 +306,24 @@ describe("product api interaction with details", () => {
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useProductDetails("12");
- const api = useProductAPI();
+ const { lib: api } = useMerchantApiContext();
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- description: "this is a description",
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // description: "this is a description",
+ // });
env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), {
request: {
@@ -337,7 +337,7 @@ describe("product api interaction with details", () => {
} as TalerMerchantApi.ProductDetail,
});
- api.updateProduct("12", {
+ api.instance.updateProduct(undefined, "12", {
description: "other description",
} as any);
},
@@ -345,12 +345,12 @@ describe("product api interaction with details", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).false;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- description: "other description",
- });
+ // expect(query.loading).false;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // description: "other description",
+ // });
},
],
env.buildTestingContext(),
diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts
index c0ace0d32..defda5552 100644
--- a/packages/merchant-backoffice-ui/src/hooks/product.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/product.ts
@@ -13,165 +13,91 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- HttpResponse,
- HttpResponseOk,
- RequestError,
-} from "@gnu-taler/web-util/browser";
-import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import _useSWR, { SWRHook, useSWRConfig } from "swr";
-import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { AccessToken, OperationOk, TalerHttpError, TalerMerchantApi, TalerMerchantManagementErrorsByMethod, TalerMerchantManagementResultByMethod, opFixedSuccess } from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
+import { buildPaginatedResult } from "./webhooks.js";
const useSWR = _useSWR as unknown as SWRHook;
-export interface ProductAPI {
- getProduct: (
- id: string,
- ) => Promise<void>;
- createProduct: (
- data: TalerMerchantApi.ProductAddDetail,
- ) => Promise<void>;
- updateProduct: (
- id: string,
- data: TalerMerchantApi.ProductPatchDetail,
- ) => Promise<void>;
- deleteProduct: (id: string) => Promise<void>;
- lockProduct: (
- id: string,
- data: TalerMerchantApi.LockRequest,
- ) => Promise<void>;
+type ProductWithId = TalerMerchantApi.ProductDetail & { id: string, serial: number };
+function notUndefined(c: ProductWithId | undefined): c is ProductWithId {
+ return c !== undefined;
}
-export function useProductAPI(): ProductAPI {
- const mutateAll = useMatchMutate();
- const { mutate } = useSWRConfig();
-
- const { request } = useBackendInstanceRequest();
-
- const createProduct = async (
- data: TalerMerchantApi.ProductAddDetail,
- ): Promise<void> => {
- const res = await request(`/private/products`, {
- method: "POST",
- data,
- });
-
- return await mutateAll(/.*\/private\/products.*/);
- };
-
- const updateProduct = async (
- productId: string,
- data: TalerMerchantApi.ProductPatchDetail,
- ): Promise<void> => {
- const r = await request(`/private/products/${productId}`, {
- method: "PATCH",
- data,
- });
+export function revalidateInstanceProducts() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listProductsWithId",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceProducts() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
- return await mutateAll(/.*\/private\/products.*/);
- };
+ const [offset, setOffset] = useState<number | undefined>();
- const deleteProduct = async (productId: string): Promise<void> => {
- await request(`/private/products/${productId}`, {
- method: "DELETE",
- });
- await mutate([`/private/products`]);
- };
-
- const lockProduct = async (
- productId: string,
- data: TalerMerchantApi.LockRequest,
- ): Promise<void> => {
- await request(`/private/products/${productId}/lock`, {
- method: "POST",
- data,
+ async function fetcher([token, bid]: [AccessToken, number]) {
+ const list = await instance.listProducts(token, {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: bid === undefined ? undefined: String(bid),
+ order: "dec",
});
+ if (list.type !== "ok") {
+ return list;
+ }
+ const all: Array<ProductWithId | undefined> = await Promise.all(
+ list.body.products.map(async (c) => {
+ const r = await instance.getProductDetails(token, c.product_id);
+ if (r.type === "fail") {
+ return undefined;
+ }
+ return { ...r.body, id: c.product_id, serial: c.product_serial };
+ }),
+ );
+ const products = all.filter(notUndefined);
+
+ return opFixedSuccess({ products });
+ }
- return await mutateAll(/.*"\/private\/products.*/);
- };
-
- const getProduct = async (
- productId: string,
- ): Promise<void> => {
- await request(`/private/products/${productId}`, {
- method: "GET",
- });
+ const { data, error } = useSWR<
+ OperationOk<{ products: ProductWithId[] }> |
+ TalerMerchantManagementErrorsByMethod<"listProducts">,
+ TalerHttpError
+ >([session.token, offset, "listProductsWithId"], fetcher);
- return
- };
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
- return { createProduct, updateProduct, deleteProduct, lockProduct, getProduct };
+ return buildPaginatedResult(data.body.products, offset, setOffset, (d) => d.serial)
}
-export function useInstanceProducts(): HttpResponse<
- (TalerMerchantApi.ProductDetail & WithId)[],
- TalerErrorDetail
-> {
- const { fetcher, multiFetcher } = useBackendInstanceRequest();
-
- const { data: list, error: listError } = useSWR<
- HttpResponseOk<TalerMerchantApi.InventorySummaryResponse>,
- RequestError<TalerErrorDetail>
- >([`/private/products`], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- const paths = (list?.data.products || []).map(
- (p) => `/private/products/${p.product_id}`,
+export function revalidateProductDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getProductDetails",
+ undefined,
+ { revalidate: true },
);
- const { data: products, error: productError } = useSWR<
- HttpResponseOk<TalerMerchantApi.ProductDetail>[],
- RequestError<TalerErrorDetail>
- >([paths], multiFetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- if (listError) return listError.cause;
- if (productError) return productError.cause;
-
- if (products) {
- const dataWithId = products.map((d) => {
- //take the id from the queried url
- return {
- ...d.data,
- id: d.info?.url.replace(/.*\/private\/products\//, "") || "",
- };
- });
- return { ok: true, data: dataWithId };
- }
- return { loading: true };
}
+export function useProductDetails(productId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ async function fetcher([pid, token]: [string, AccessToken]) {
+ return await instance.getProductDetails(token, pid);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getProductDetails">,
+ TalerHttpError
+ >([productId, session.token, "getProductDetails"], fetcher);
-export function useProductDetails(
- productId: string,
-): HttpResponse<
- TalerMerchantApi.ProductDetail,
- TalerErrorDetail
-> {
- const { fetcher } = useBackendInstanceRequest();
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<TalerMerchantApi.ProductDetail>,
- RequestError<TalerErrorDetail>
- >([`/private/products/${productId}`], fetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
- if (error) return error.cause;
- return { loading: true };
+ if (error) return error;
+ return undefined;
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts
index 2da02e3c7..12d99f3fc 100644
--- a/packages/merchant-backoffice-ui/src/hooks/templates.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts
@@ -13,254 +13,75 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- HttpResponse,
- HttpResponseOk,
- HttpResponsePaginated,
- RequestError,
-} from "@gnu-taler/web-util/browser";
-import { useEffect, useState } from "preact/hooks";
-import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
-import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+import { useState } from "preact/hooks";
+import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
-import _useSWR, { SWRHook } from "swr";
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+import { buildPaginatedResult } from "./webhooks.js";
const useSWR = _useSWR as unknown as SWRHook;
-export function useTemplateAPI(): TemplateAPI {
- const mutateAll = useMatchMutate();
- const { request } = useBackendInstanceRequest();
-
- const createTemplate = async (
- data: TalerMerchantApi.TemplateAddDetails,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`/private/templates`, {
- method: "POST",
- data,
- });
- await mutateAll(/.*private\/templates.*/);
- return res;
- };
-
- const updateTemplate = async (
- templateId: string,
- data: TalerMerchantApi.TemplatePatchDetails,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`/private/templates/${templateId}`, {
- method: "PATCH",
- data,
- });
- await mutateAll(/.*private\/templates.*/);
- return res;
- };
-
- const deleteTemplate = async (
- templateId: string,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`/private/templates/${templateId}`, {
- method: "DELETE",
- });
- await mutateAll(/.*private\/templates.*/);
- return res;
- };
-
- const createOrderFromTemplate = async (
- templateId: string,
- data: TalerMerchantApi.UsingTemplateDetails,
- ): Promise<
- HttpResponseOk<TalerMerchantApi.PostOrderResponse>
- > => {
- const res = await request<TalerMerchantApi.PostOrderResponse>(
- `/templates/${templateId}`,
- {
- method: "POST",
- data,
- },
- );
- await mutateAll(/.*private\/templates.*/);
- return res;
- };
-
- const testTemplateExist = async (
- templateId: string,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", });
- return res;
- };
-
-
- return {
- createTemplate,
- updateTemplate,
- deleteTemplate,
- testTemplateExist,
- createOrderFromTemplate,
- };
-}
-
-export interface TemplateAPI {
- createTemplate: (
- data: TalerMerchantApi.TemplateAddDetails,
- ) => Promise<HttpResponseOk<void>>;
- updateTemplate: (
- id: string,
- data: TalerMerchantApi.TemplatePatchDetails,
- ) => Promise<HttpResponseOk<void>>;
- testTemplateExist: (
- id: string
- ) => Promise<HttpResponseOk<void>>;
- deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>;
- createOrderFromTemplate: (
- id: string,
- data: TalerMerchantApi.UsingTemplateDetails,
- ) => Promise<HttpResponseOk<TalerMerchantApi.PostOrderResponse>>;
-}
export interface InstanceTemplateFilter {
- //FIXME: add filter to the template list
- position?: string;
}
-export function useInstanceTemplates(
- args?: InstanceTemplateFilter,
- updatePosition?: (id: string) => void,
-): HttpResponsePaginated<
- TalerMerchantApi.TemplateSummaryResponse,
- TalerErrorDetail
-> {
- const { templateFetcher } = useBackendInstanceRequest();
-
- const [pageBefore, setPageBefore] = useState(1);
- const [pageAfter, setPageAfter] = useState(1);
-
- const totalAfter = pageAfter * PAGE_SIZE;
- const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0;
+export function revalidateInstanceTemplates() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listTemplates",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceTemplates() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
- /**
- * FIXME: this can be cleaned up a little
- *
- * the logic of double query should be inside the orderFetch so from the hook perspective and cache
- * is just one query and one error status
- */
- const {
- data: beforeData,
- error: beforeError,
- isValidating: loadingBefore,
- } = useSWR<
- HttpResponseOk<TalerMerchantApi.TemplateSummaryResponse>,
- RequestError<TalerErrorDetail>>(
- [
- `/private/templates`,
- args?.position,
- totalBefore,
- ],
- templateFetcher,
- );
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<
- HttpResponseOk<TalerMerchantApi.TemplateSummaryResponse>,
- RequestError<TalerErrorDetail>
- >([`/private/templates`, args?.position, -totalAfter], templateFetcher);
+ const [offset, setOffset] = useState<string | undefined>();
- //this will save last result
- const [lastBefore, setLastBefore] = useState<
- HttpResponse<
- TalerMerchantApi.TemplateSummaryResponse,
- TalerErrorDetail
- >
- >({ loading: true });
+ async function fetcher([token, bid]: [AccessToken, string]) {
+ return await instance.listTemplates(token, {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: bid,
+ order: "dec",
+ });
+ }
- const [lastAfter, setLastAfter] = useState<
- HttpResponse<
- TalerMerchantApi.TemplateSummaryResponse,
- TalerErrorDetail
- >
- >({ loading: true });
- useEffect(() => {
- if (afterData) setLastAfter(afterData);
- if (beforeData) setLastBefore(beforeData);
- }, [afterData, beforeData]);
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listTemplates">,
+ TalerHttpError
+ >([session.token, offset, "listTemplates"], fetcher);
- if (beforeError) return beforeError.cause;
- if (afterError) return afterError.cause;
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
- // if the query returns less that we ask, then we have reach the end or beginning
- const isReachingEnd =
- afterData && afterData.data.templates.length < totalAfter;
- const isReachingStart = args?.position === undefined
- ||
- (beforeData && beforeData.data.templates.length < totalBefore);
+ return buildPaginatedResult(data.body.templates, offset, setOffset, (d) => d.template_id)
- const pagination = {
- isReachingEnd,
- isReachingStart,
- loadMore: () => {
- if (!afterData || isReachingEnd) return;
- if (afterData.data.templates.length < MAX_RESULT_SIZE) {
- setPageAfter(pageAfter + 1);
- } else {
- const from = `${afterData.data.templates[afterData.data.templates.length - 1]
- .template_id
- }`;
- if (from && updatePosition) updatePosition(from);
- }
- },
- loadMorePrev: () => {
- if (!beforeData || isReachingStart) return;
- if (beforeData.data.templates.length < MAX_RESULT_SIZE) {
- setPageBefore(pageBefore + 1);
- } else if (beforeData) {
- const from = `${beforeData.data.templates[beforeData.data.templates.length - 1]
- .template_id
- }`;
- if (from && updatePosition) updatePosition(from);
- }
- },
- };
+}
- // const templates = !afterData ? [] : (afterData || lastAfter).data.templates;
- const templates =
- !beforeData || !afterData
- ? []
- : (beforeData || lastBefore).data.templates
- .slice()
- .reverse()
- .concat((afterData || lastAfter).data.templates);
- if (loadingAfter || loadingBefore)
- return { loading: true, data: { templates } };
- if (beforeData && afterData) {
- return { ok: true, data: { templates }, ...pagination };
- }
- return { loading: true };
+export function revalidateTemplateDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getTemplateDetails",
+ undefined,
+ { revalidate: true },
+ );
}
+export function useTemplateDetails(templateId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
-export function useTemplateDetails(
- templateId: string,
-): HttpResponse<
- TalerMerchantApi.TemplateDetails,
- TalerErrorDetail
-> {
- const { templateFetcher } = useBackendInstanceRequest();
+ async function fetcher([tid, token]: [string, AccessToken]) {
+ return await instance.getTemplateDetails(token, tid);
+ }
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<TalerMerchantApi.TemplateDetails>,
- RequestError<TalerErrorDetail>
- >([`/private/templates/${templateId}`], templateFetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getTemplateDetails">,
+ TalerHttpError
+ >([templateId, session.token, "getTemplateDetails"], fetcher);
- if (isValidating) return { loading: true, data: data?.data };
- if (data) {
- return data;
- }
- if (error) return error.cause;
- return { loading: true };
+ if (data) return data;
+ if (error) return error;
+ return undefined;
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/testing.tsx b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
index bebf7716b..fc78f6c58 100644
--- a/packages/merchant-backoffice-ui/src/hooks/testing.tsx
+++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
@@ -24,9 +24,23 @@ import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http";
import { SWRConfig } from "swr";
import { ApiContextProvider } from "@gnu-taler/web-util/browser";
-import { HttpResponseOk, RequestOptions } from "@gnu-taler/web-util/browser";
import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util";
+interface RequestOptions {
+ method?: "GET" | "POST" | "HEAD",
+ params?: any,
+ token?: string | undefined,
+ data?: any,
+}
+interface HttpResponseOk<T> {
+ ok: true,
+ data: T,
+ loading: boolean,
+ clientError: boolean,
+ serverError: boolean,
+ info: any,
+}
+
export class ApiMockEnvironment extends MockEnvironment {
constructor(debug = false) {
super(debug);
@@ -143,7 +157,7 @@ export class ApiMockEnvironment extends MockEnvironment {
}
const bankCore = new TalerCoreBankHttpClient("http://localhost", mockHttpClient)
const bankIntegration = new TalerBankIntegrationHttpClient(bankCore.getIntegrationAPI().href, mockHttpClient)
- const bankRevenue = new TalerRevenueHttpClient(bankCore.getRevenueAPI("a").href, "a", mockHttpClient)
+ const bankRevenue = new TalerRevenueHttpClient(bankCore.getRevenueAPI("a").href, mockHttpClient)
const bankWire = new TalerWireGatewayHttpClient(bankCore.getWireGatewayAPI("b").href, "b", mockHttpClient)
return (
@@ -156,7 +170,7 @@ export class ApiMockEnvironment extends MockEnvironment {
// changeToken: () => null,
// }}
// >
- <ApiContextProvider value={{ request, bankCore, bankIntegration, bankRevenue, bankWire }}>
+ <ApiContextProvider value={{ request : undefined as any, bankCore, bankIntegration, bankRevenue, bankWire }}>
<SC
value={{
loadingTimeout: 0,
diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts
index ee987af7e..7daaf5049 100644
--- a/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts
@@ -19,12 +19,17 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AmountString, PaytoString, TalerMerchantApi } from "@gnu-taler/taler-util";
+import {
+ AmountString,
+ PaytoString,
+ TalerMerchantApi,
+} from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import { expect } from "chai";
import { ApiMockEnvironment } from "./testing.js";
-import { useInstanceTransfers, useTransferAPI } from "./transfer.js";
+import { useInstanceTransfers } from "./transfer.js";
import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js";
+import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
describe("transfer api interaction with listing", () => {
it("should evict cache when informing a transfer", async () => {
@@ -37,32 +42,32 @@ describe("transfer api interaction with listing", () => {
},
});
- const moveCursor = (d: string) => {
+ const moveCursor = (d: string | undefined) => {
console.log("new position", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceTransfers({}, moveCursor);
- const api = useTransferAPI();
+ const { lib: api } = useMerchantApiContext();
return { query, api };
},
{},
[
({ query, api }) => {
- expect(query.loading).true;
+ // expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- transfers: [{ wtid: "2" }],
- });
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // transfers: [{ wtid: "2" }],
+ // });
env.addRequestExpectation(API_INFORM_TRANSFERS, {
request: {
@@ -81,7 +86,7 @@ describe("transfer api interaction with listing", () => {
},
});
- api.informTransfer({
+ api.instance.informWireTransfer(undefined, {
wtid: "3",
credit_amount: "EUR:1" as AmountString,
exchange_url: "exchange.url",
@@ -92,13 +97,13 @@ describe("transfer api interaction with listing", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
- expect(query.data).deep.equals({
- transfers: [{ wtid: "3" }, { wtid: "2" }],
- });
+ // expect(query.data).deep.equals({
+ // transfers: [{ wtid: "3" }, { wtid: "2" }],
+ // });
},
],
env.buildTestingContext(),
@@ -120,12 +125,16 @@ describe("transfer listing pagination", () => {
},
});
- const moveCursor = (d: string) => {
+ const moveCursor = (d: string | undefined) => {
console.log("new position", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
- return useInstanceTransfers({ payto_uri: "payto://" }, moveCursor);
+ const query = useInstanceTransfers(
+ { payto_uri: "payto://" },
+ moveCursor,
+ );
+ return { query };
},
{},
[
@@ -133,22 +142,18 @@ describe("transfer listing pagination", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(query.loading).true;
+ // expect(query.loading).true;
},
(query) => {
- expect(query.loading).undefined;
- expect(query.ok).true;
- if (!query.ok) return;
- expect(query.data).deep.equals({
- transfers: [{ wtid: "2" }, { wtid: "1" }],
- });
- expect(query.isReachingEnd).true;
- expect(query.isReachingStart).true;
+ // expect(query.loading).undefined;
+ // expect(query.ok).true;
+ // if (!query.ok) return;
+ // expect(query.data).deep.equals({
+ // transfers: [{ wtid: "2" }, { wtid: "1" }],
+ // });
+ // expect(query.isReachingEnd).true;
+ // expect(query.isReachingStart).true;
- //check that this button won't trigger more updates since
- //has reach end and start
- query.loadMore();
- query.loadMorePrev();
},
],
env.buildTestingContext(),
@@ -158,7 +163,7 @@ describe("transfer listing pagination", () => {
expect(hookBehavior).deep.eq({ result: "ok" });
});
- it("should load more if result brings more that PAGE_SIZE", async () => {
+ it("should load more if result brings more that PAGINATED_LIST_REQUEST", async () => {
const env = new ApiMockEnvironment();
const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({
@@ -167,7 +172,7 @@ describe("transfer listing pagination", () => {
const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({
wtid: String(i + 20),
}));
- const transfersFrom20to0 = [...transfersFrom0to20].reverse();
+ // const transfersFrom20to0 = [...transfersFrom0to20].reverse();
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: 20, payto_uri: "payto://", offset: "1" },
@@ -183,16 +188,17 @@ describe("transfer listing pagination", () => {
},
});
- const moveCursor = (d: string) => {
+ const moveCursor = (d: string | undefined) => {
console.log("new position", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
- return useInstanceTransfers(
+ const query = useInstanceTransfers(
{ payto_uri: "payto://", position: "1" },
moveCursor,
);
+ return { query };
},
{},
[
@@ -200,17 +206,17 @@ describe("transfer listing pagination", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(result.loading).true;
+ // expect(result.loading).true;
},
(result) => {
- expect(result.loading).undefined;
- expect(result.ok).true;
- if (!result.ok) return;
- expect(result.data).deep.equals({
- transfers: [...transfersFrom20to0, ...transfersFrom20to40],
- });
- expect(result.isReachingEnd).false;
- expect(result.isReachingStart).false;
+ // expect(result.loading).undefined;
+ // expect(result.ok).true;
+ // if (!result.ok) return;
+ // expect(result.data).deep.equals({
+ // transfers: [...transfersFrom20to0, ...transfersFrom20to40],
+ // });
+ // expect(result.isReachingEnd).false;
+ // expect(result.isReachingStart).false;
//query more
env.addRequestExpectation(API_LIST_TRANSFERS, {
@@ -219,30 +225,30 @@ describe("transfer listing pagination", () => {
transfers: [...transfersFrom20to40, { wtid: "41" }],
},
});
- result.loadMore();
+ // result.loadMore();
},
(result) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(result.loading).true;
+ // expect(result.loading).true;
},
(result) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
- expect(result.loading).undefined;
- expect(result.ok).true;
- if (!result.ok) return;
- expect(result.data).deep.equals({
- transfers: [
- ...transfersFrom20to0,
- ...transfersFrom20to40,
- { wtid: "41" },
- ],
- });
- expect(result.isReachingEnd).true;
- expect(result.isReachingStart).false;
+ // expect(result.loading).undefined;
+ // expect(result.ok).true;
+ // if (!result.ok) return;
+ // expect(result.data).deep.equals({
+ // transfers: [
+ // ...transfersFrom20to0,
+ // ...transfersFrom20to40,
+ // { wtid: "41" },
+ // ],
+ // });
+ // expect(result.isReachingEnd).true;
+ // expect(result.isReachingStart).false;
},
],
env.buildTestingContext(),
diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts
index 20062a5e2..6f77369c2 100644
--- a/packages/merchant-backoffice-ui/src/hooks/transfer.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/transfer.ts
@@ -13,176 +13,56 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- HttpResponse,
- HttpResponseOk,
- HttpResponsePaginated,
- RequestError,
-} from "@gnu-taler/web-util/browser";
-import { useEffect, useState } from "preact/hooks";
-import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
-import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
-import _useSWR, { SWRHook } from "swr";
+import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
+import { buildPaginatedResult } from "./webhooks.js";
const useSWR = _useSWR as unknown as SWRHook;
-export function useTransferAPI(): TransferAPI {
- const mutateAll = useMatchMutate();
- const { request } = useBackendInstanceRequest();
-
- const informTransfer = async (
- data: TalerMerchantApi.TransferInformation,
- ): Promise<HttpResponseOk<{}>> => {
- const res = await request<{}>(`/private/transfers`, {
- method: "POST",
- data,
- });
-
- await mutateAll(/.*private\/transfers.*/);
- return res;
- };
-
- return { informTransfer };
-}
-
-export interface TransferAPI {
- informTransfer: (
- data: TalerMerchantApi.TransferInformation,
- ) => Promise<HttpResponseOk<{}>>;
-}
-
export interface InstanceTransferFilter {
payto_uri?: string;
- verified?: "yes" | "no";
+ verified?: boolean;
position?: string;
}
+export function revalidateInstanceTransfers() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listWireTransfers",
+ undefined,
+ { revalidate: true },
+ );
+}
export function useInstanceTransfers(
args?: InstanceTransferFilter,
- updatePosition?: (id: string) => void,
-): HttpResponsePaginated<
- TalerMerchantApi.TransferList,
- TalerErrorDetail
-> {
- const { transferFetcher } = useBackendInstanceRequest();
-
- const [pageBefore, setPageBefore] = useState(1);
- const [pageAfter, setPageAfter] = useState(1);
-
- const totalAfter = pageAfter * PAGE_SIZE;
- const totalBefore = args?.position !== undefined ? pageBefore * PAGE_SIZE : 0;
-
- /**
- * FIXME: this can be cleaned up a little
- *
- * the logic of double query should be inside the orderFetch so from the hook perspective and cache
- * is just one query and one error status
- */
- const {
- data: beforeData,
- error: beforeError,
- isValidating: loadingBefore,
- } = useSWR<
- HttpResponseOk<TalerMerchantApi.TransferList>,
- RequestError<TalerErrorDetail>
- >(
- [
- `/private/transfers`,
- args?.payto_uri,
- args?.verified,
- args?.position,
- totalBefore,
- ],
- transferFetcher,
- );
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<
- HttpResponseOk<TalerMerchantApi.TransferList>,
- RequestError<TalerErrorDetail>
- >(
- [
- `/private/transfers`,
- args?.payto_uri,
- args?.verified,
- args?.position,
- -totalAfter,
- ],
- transferFetcher,
- );
-
- //this will save last result
- const [lastBefore, setLastBefore] = useState<
- HttpResponse<
- TalerMerchantApi.TransferList,
- TalerErrorDetail
- >
- >({ loading: true });
- const [lastAfter, setLastAfter] = useState<
- HttpResponse<
- TalerMerchantApi.TransferList,
- TalerErrorDetail
- >
- >({ loading: true });
- useEffect(() => {
- if (afterData) setLastAfter(afterData);
- if (beforeData) setLastBefore(beforeData);
- }, [afterData, beforeData]);
+ updatePosition: (id: string | undefined) => void = (() => { }),
+) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ // const [offset, setOffset] = useState<string | undefined>(args?.position);
+
+ async function fetcher([token, o, p, v]: [AccessToken, string, string, boolean]) {
+ return await instance.listWireTransfers(token, {
+ paytoURI: p,
+ verified: v,
+ limit: PAGINATED_LIST_REQUEST,
+ offset: o,
+ order: "dec",
+ });
+ }
- if (beforeError) return beforeError.cause;
- if (afterError) return afterError.cause;
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listWireTransfers">,
+ TalerHttpError
+ >([session.token, args?.position, args?.payto_uri, args?.verified, "listWireTransfers"], fetcher);
- // if the query returns less that we ask, then we have reach the end or beginning
- const isReachingEnd =
- afterData && afterData.data.transfers.length < totalAfter;
- const isReachingStart =
- args?.position === undefined ||
- (beforeData && beforeData.data.transfers.length < totalBefore);
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
- const pagination = {
- isReachingEnd,
- isReachingStart,
- loadMore: () => {
- if (!afterData || isReachingEnd) return;
- if (afterData.data.transfers.length < MAX_RESULT_SIZE) {
- setPageAfter(pageAfter + 1);
- } else {
- const from = `${
- afterData.data.transfers[afterData.data.transfers.length - 1]
- .transfer_serial_id
- }`;
- if (from && updatePosition) updatePosition(from);
- }
- },
- loadMorePrev: () => {
- if (!beforeData || isReachingStart) return;
- if (beforeData.data.transfers.length < MAX_RESULT_SIZE) {
- setPageBefore(pageBefore + 1);
- } else if (beforeData) {
- const from = `${
- beforeData.data.transfers[beforeData.data.transfers.length - 1]
- .transfer_serial_id
- }`;
- if (from && updatePosition) updatePosition(from);
- }
- },
- };
+ return buildPaginatedResult(data.body.transfers, args?.position, updatePosition, (d) => String(d.transfer_serial_id))
- const transfers =
- !beforeData || !afterData
- ? []
- : (beforeData || lastBefore).data.transfers
- .slice()
- .reverse()
- .concat((afterData || lastAfter).data.transfers);
- if (loadingAfter || loadingBefore)
- return { loading: true, data: { transfers } };
- if (beforeData && afterData) {
- return { ok: true, data: { transfers }, ...pagination };
- }
- return { loading: true };
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
index 4e62a81c9..fe37162aa 100644
--- a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts
@@ -13,167 +13,106 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- HttpResponse,
- HttpResponseOk,
- HttpResponsePaginated,
- RequestError,
-} from "@gnu-taler/web-util/browser";
-import { useEffect, useState } from "preact/hooks";
-import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
-import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
+import { PAGINATED_LIST_REQUEST } from "../utils/constants.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import { TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
-import _useSWR, { SWRHook } from "swr";
+import { AccessToken, OperationOk, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useSessionContext } from "../context/session.js";
const useSWR = _useSWR as unknown as SWRHook;
-export function useWebhookAPI(): WebhookAPI {
- const mutateAll = useMatchMutate();
- const { request } = useBackendInstanceRequest();
+export interface InstanceWebhookFilter {
+}
- const createWebhook = async (
- data: TalerMerchantApi.WebhookAddDetails,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`/private/webhooks`, {
- method: "POST",
- data,
- });
- await mutateAll(/.*private\/webhooks.*/);
- return res;
- };
+export function revalidateInstanceWebhooks() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "listWebhooks",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useInstanceWebhooks() {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
- const updateWebhook = async (
- webhookId: string,
- data: TalerMerchantApi.WebhookPatchDetails,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`/private/webhooks/${webhookId}`, {
- method: "PATCH",
- data,
- });
- await mutateAll(/.*private\/webhooks.*/);
- return res;
- };
+ // const [offset, setOffset] = useState<string | undefined>();
- const deleteWebhook = async (
- webhookId: string,
- ): Promise<HttpResponseOk<void>> => {
- const res = await request<void>(`/private/webhooks/${webhookId}`, {
- method: "DELETE",
+ async function fetcher([token, _bid]: [AccessToken, string]) {
+ return await instance.listWebhooks(token, {
+ // limit: PAGINATED_LIST_REQUEST,
+ // offset: bid,
+ // order: "dec",
});
- await mutateAll(/.*private\/webhooks.*/);
- return res;
- };
+ }
- return { createWebhook, updateWebhook, deleteWebhook };
-}
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"listWebhooks">,
+ TalerHttpError
+ >([session.token, "offset", "listWebhooks"], fetcher);
-export interface WebhookAPI {
- createWebhook: (
- data: TalerMerchantApi.WebhookAddDetails,
- ) => Promise<HttpResponseOk<void>>;
- updateWebhook: (
- id: string,
- data: TalerMerchantApi.WebhookPatchDetails,
- ) => Promise<HttpResponseOk<void>>;
- deleteWebhook: (id: string) => Promise<HttpResponseOk<void>>;
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ // return buildPaginatedResult(data.body.webhooks, offset, setOffset, (d) => d.webhook_id)
+ return data;
}
-export interface InstanceWebhookFilter {
- //FIXME: add filter to the webhook list
- position?: string;
+type PaginatedResult<T> = OperationOk<T> & {
+ isLastPage: boolean;
+ isFirstPage: boolean;
+ loadNext(): void;
+ loadFirst(): void;
}
-export function useInstanceWebhooks(
- args?: InstanceWebhookFilter,
- updatePosition?: (id: string) => void,
-): HttpResponsePaginated<
- TalerMerchantApi.WebhookSummaryResponse,
- TalerErrorDetail
-> {
- const { webhookFetcher } = useBackendInstanceRequest();
-
- const [pageBefore, setPageBefore] = useState(1);
- const [pageAfter, setPageAfter] = useState(1);
-
- const totalAfter = pageAfter * PAGE_SIZE;
- const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0;
-
- const {
- data: afterData,
- error: afterError,
- isValidating: loadingAfter,
- } = useSWR<
- HttpResponseOk<TalerMerchantApi.WebhookSummaryResponse>,
- RequestError<TalerErrorDetail>
-
- >([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher);
-
- const [lastAfter, setLastAfter] = useState<
- HttpResponse<
- TalerMerchantApi.WebhookSummaryResponse,
- TalerErrorDetail
- >
- >({ loading: true });
- useEffect(() => {
- if (afterData) setLastAfter(afterData);
- }, [afterData]);
-
- if (afterError) return afterError.cause;
-
- const isReachingEnd =
- afterData && afterData.data.webhooks.length < totalAfter;
- const isReachingStart = true;
-
- const pagination = {
- isReachingEnd,
- isReachingStart,
- loadMore: () => {
- if (!afterData || isReachingEnd) return;
- if (afterData.data.webhooks.length < MAX_RESULT_SIZE) {
- setPageAfter(pageAfter + 1);
- } else {
- const from = `${
- afterData.data.webhooks[afterData.data.webhooks.length - 1].webhook_id
- }`;
- if (from && updatePosition) updatePosition(from);
- }
+//TODO: consider sending this to web-util
+export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefined, setOffset: (o: OffId | undefined) => void, getId: (r: R) => OffId): PaginatedResult<R[]> {
+
+ const isLastPage = data.length < PAGINATED_LIST_REQUEST;
+ const isFirstPage = offset === undefined;
+
+ const result = structuredClone(data);
+ if (result.length == PAGINATED_LIST_REQUEST) {
+ result.pop();
+ }
+ return {
+ type: "ok",
+ body: result,
+ isLastPage,
+ isFirstPage,
+ loadNext: () => {
+ if (!result.length) return;
+ const id = getId(result[result.length - 1])
+ setOffset(id);
},
- loadMorePrev: () => {
- return;
+ loadFirst: () => {
+ setOffset(undefined);
},
};
+}
- const webhooks = !afterData ? [] : (afterData || lastAfter).data.webhooks;
- if (loadingAfter) return { loading: true, data: { webhooks } };
- if (afterData) {
- return { ok: true, data: { webhooks }, ...pagination };
- }
- return { loading: true };
+export function revalidateWebhookDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getWebhookDetails",
+ undefined,
+ { revalidate: true },
+ );
}
+export function useWebhookDetails(webhookId: string) {
+ const { state: session } = useSessionContext();
+ const { lib: { instance } } = useSessionContext();
+
+ async function fetcher([hookId, token]: [string, AccessToken]) {
+ return await instance.getWebhookDetails(token, hookId);
+ }
+
+ const { data, error } = useSWR<
+ TalerMerchantManagementResultByMethod<"getWebhookDetails">,
+ TalerHttpError
+ >([webhookId, session.token, "getWebhookDetails"], fetcher);
-export function useWebhookDetails(
- webhookId: string,
-): HttpResponse<
- TalerMerchantApi.WebhookDetails,
- TalerErrorDetail
-> {
- const { webhookFetcher } = useBackendInstanceRequest();
-
- const { data, error, isValidating } = useSWR<
- HttpResponseOk<TalerMerchantApi.WebhookDetails>,
- RequestError<TalerErrorDetail>
- >([`/private/webhooks/${webhookId}`], webhookFetcher, {
- refreshInterval: 0,
- refreshWhenHidden: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshWhenOffline: false,
- });
-
- if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
- if (error) return error.cause;
- return { loading: true };
+ if (error) return error;
+ return undefined;
}
diff --git a/packages/merchant-backoffice-ui/src/i18n/de.po b/packages/merchant-backoffice-ui/src/i18n/de.po
index f34d5dd20..66d654f64 100644
--- a/packages/merchant-backoffice-ui/src/i18n/de.po
+++ b/packages/merchant-backoffice-ui/src/i18n/de.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2024-03-21 21:39+0000\n"
+"PO-Revision-Date: 2024-05-07 14:32+0000\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"merchant-backoffice/de/>\n"
@@ -26,27 +26,27 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 5.2.1\n"
+"X-Generator: Weblate 5.4.3\n"
#: src/components/modal/index.tsx:71
#, c-format
msgid "Cancel"
-msgstr ""
+msgstr "Zurück"
#: src/components/modal/index.tsx:79
#, c-format
msgid "%1$s"
-msgstr ""
+msgstr "%1$s"
#: src/components/modal/index.tsx:84
#, c-format
msgid "Close"
-msgstr ""
+msgstr "Schließen"
#: src/components/modal/index.tsx:124
#, c-format
msgid "Continue"
-msgstr ""
+msgstr "Weiter"
#: src/components/modal/index.tsx:178
#, c-format
@@ -66,12 +66,12 @@ msgstr ""
#: src/components/modal/index.tsx:299
#, c-format
msgid "cannot be empty"
-msgstr ""
+msgstr "darf nicht leer sein"
#: src/components/modal/index.tsx:301
#, c-format
msgid "cannot be the same as the old token"
-msgstr ""
+msgstr "muss sich vom alten Token unterscheiden"
#: src/components/modal/index.tsx:305
#, c-format
@@ -563,57 +563,59 @@ msgstr ""
#: src/paths/instance/orders/create/CreatePage.tsx:164
#, c-format
msgid "not a valid json"
-msgstr ""
+msgstr "kein gültiges JSON-Format"
#: src/paths/instance/orders/create/CreatePage.tsx:170
#, c-format
msgid "should be in the future"
-msgstr ""
+msgstr "sollte in der Zukunft liegen"
#: src/paths/instance/orders/create/CreatePage.tsx:173
#, c-format
msgid "refund deadline cannot be before pay deadline"
-msgstr ""
+msgstr "Die Rückerstattungsfrist kann nicht vor der Zahlungsfrist liegen"
#: src/paths/instance/orders/create/CreatePage.tsx:179
#, c-format
msgid "wire transfer deadline cannot be before refund deadline"
-msgstr ""
+msgstr "Die Überweisungsfrist kann nicht vor der Rückerstattungsfrist liegen"
#: src/paths/instance/orders/create/CreatePage.tsx:190
#, c-format
msgid "wire transfer deadline cannot be before pay deadline"
-msgstr ""
+msgstr "Die Ãœberweisungsfrist kann nicht vor der Zahlungsfrist liegen"
#: src/paths/instance/orders/create/CreatePage.tsx:197
#, c-format
msgid "should have a refund deadline"
-msgstr ""
+msgstr "sollte eine Rückerstattungsfrist haben"
#: src/paths/instance/orders/create/CreatePage.tsx:202
#, c-format
msgid "auto refund cannot be after refund deadline"
msgstr ""
+"Die automatische Rückerstattung kann nicht nach der Rückerstattungsfrist "
+"erfolgen"
#: src/paths/instance/orders/create/CreatePage.tsx:360
#, c-format
msgid "Manage products in order"
-msgstr ""
+msgstr "Artikel in der Bestellung verwalten"
#: src/paths/instance/orders/create/CreatePage.tsx:369
#, c-format
msgid "Manage list of products in the order."
-msgstr ""
+msgstr "Liste der Artikel in der Bestellung verwalten."
#: src/paths/instance/orders/create/CreatePage.tsx:391
#, c-format
msgid "Remove this product from the order."
-msgstr ""
+msgstr "Diesen Artikel aus der Bestellung entfernen."
#: src/paths/instance/orders/create/CreatePage.tsx:415
#, c-format
msgid "Total price"
-msgstr ""
+msgstr "Gesamtpreis"
#: src/paths/instance/orders/create/CreatePage.tsx:417
#, c-format
@@ -623,12 +625,12 @@ msgstr ""
#: src/paths/instance/orders/create/CreatePage.tsx:430
#, c-format
msgid "Amount to be paid by the customer"
-msgstr ""
+msgstr "Zu zahlender Betrag"
#: src/paths/instance/orders/create/CreatePage.tsx:436
#, c-format
msgid "Order price"
-msgstr ""
+msgstr "Bestellsumme"
#: src/paths/instance/orders/create/CreatePage.tsx:437
#, c-format
@@ -638,12 +640,12 @@ msgstr ""
#: src/paths/instance/orders/create/CreatePage.tsx:444
#, c-format
msgid "Summary"
-msgstr ""
+msgstr "Zusammenfassung"
#: src/paths/instance/orders/create/CreatePage.tsx:445
#, c-format
msgid "Title of the order to be shown to the customer"
-msgstr ""
+msgstr "Bezeichnung der Bestellung, die den Kunden angezeigt wird"
#: src/paths/instance/orders/create/CreatePage.tsx:450
#, c-format
@@ -653,12 +655,12 @@ msgstr ""
#: src/paths/instance/orders/create/CreatePage.tsx:455
#, c-format
msgid "Delivery date"
-msgstr ""
+msgstr "Lieferdatum"
#: src/paths/instance/orders/create/CreatePage.tsx:456
#, c-format
msgid "Deadline for physical delivery assured by the merchant."
-msgstr ""
+msgstr "Vom Händler zugesicherte Zustellfrist."
#: src/paths/instance/orders/create/CreatePage.tsx:461
#, c-format
@@ -668,22 +670,22 @@ msgstr ""
#: src/paths/instance/orders/create/CreatePage.tsx:462
#, c-format
msgid "address where the products will be delivered"
-msgstr ""
+msgstr "Zustelladresse der Artikel"
#: src/paths/instance/orders/create/CreatePage.tsx:469
#, c-format
msgid "Fulfillment URL"
-msgstr ""
+msgstr "Adresse digitaler Dienstleistung (Fulfillment-URL)"
#: src/paths/instance/orders/create/CreatePage.tsx:470
#, c-format
msgid "URL to which the user will be redirected after successful payment."
-msgstr ""
+msgstr "URL der von Kunden zu besuchenden Adresse nach erfolgter Bezahlung."
#: src/paths/instance/orders/create/CreatePage.tsx:476
#, c-format
msgid "Taler payment options"
-msgstr ""
+msgstr "Taler-Zahlungsoptionen"
#: src/paths/instance/orders/create/CreatePage.tsx:477
#, c-format
@@ -693,7 +695,7 @@ msgstr ""
#: src/paths/instance/orders/create/CreatePage.tsx:481
#, c-format
msgid "Payment deadline"
-msgstr ""
+msgstr "Zahlungsfrist"
#: src/paths/instance/orders/create/CreatePage.tsx:482
#, c-format
diff --git a/packages/merchant-backoffice-ui/src/index.html b/packages/merchant-backoffice-ui/src/index.html
index aed14a1de..b005f967d 100644
--- a/packages/merchant-backoffice-ui/src/index.html
+++ b/packages/merchant-backoffice-ui/src/index.html
@@ -35,7 +35,7 @@
<title>Merchant Backoffice</title>
<!-- Optional customization script. -->
<script src="merchant-backoffice-ui-settings.js"></script>
- <!-- Entry point for the demobank SPA. -->
+ <!-- Entry point for the SPA. -->
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx
index 39fdb6bdc..54d947e14 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx
@@ -19,9 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode, FunctionalComponent } from "preact";
-import { CreatePage as TestedComponent } from "./CreatePage.js";
import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser";
+import { FunctionalComponent, h } from "preact";
+import { CreatePage as TestedComponent } from "./CreatePage.js";
export default {
title: "Pages/Instance/Create",
@@ -40,6 +40,7 @@ function createExample<Props>(
<MerchantApiProviderTesting
value={{
cancelRequest: () => {},
+ changeBackend: () => {},
config: {
currency: "ARS",
version: "1",
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
index d53d93e8b..a28992a2f 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
@@ -19,7 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Duration, TalerMerchantApi } from "@gnu-taler/taler-util";
+import {
+ Duration,
+ TalerMerchantApi,
+ createRFC8959AccessTokenPlain,
+} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -33,10 +37,13 @@ import { SetTokenNewInstanceModal } from "../../../components/modal/index.js";
import { INSTANCE_ID_REGEX } from "../../../utils/constants.js";
import { undefinedIfEmpty } from "../../../utils/table.js";
-export type Entity = Omit<Omit<TalerMerchantApi.InstanceConfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & {
+export type Entity = Omit<
+ Omit<TalerMerchantApi.InstanceConfigurationMessage, "default_pay_delay">,
+ "default_wire_transfer_delay"
+> & {
auth_token?: string;
- default_pay_delay: Duration,
- default_wire_transfer_delay: Duration,
+ default_pay_delay: Duration;
+ default_wire_transfer_delay: Duration;
};
interface Props {
@@ -90,10 +97,11 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
default_pay_delay: !value.default_pay_delay
? i18n.str`required`
: !!value.default_wire_transfer_delay &&
- value.default_wire_transfer_delay.d_ms !== "forever" &&
- value.default_pay_delay.d_ms !== "forever" &&
- value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ?
- i18n.str`pay delay can't be greater than wire transfer delay` : undefined,
+ value.default_wire_transfer_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms !== "forever" &&
+ value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms
+ ? i18n.str`pay delay can't be greater than wire transfer delay`
+ : undefined,
default_wire_transfer_delay: !value.default_wire_transfer_delay
? i18n.str`required`
: undefined,
@@ -112,7 +120,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
};
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
);
const submit = (): Promise<void> => {
@@ -121,19 +129,26 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
const newToken = newValue.auth_token;
newValue.auth_token = undefined;
- newValue.auth = newToken === null || newToken === undefined
- ? { method: "external" }
- : { method: "token", token: newToken };
+ newValue.auth =
+ newToken === null || newToken === undefined
+ ? { method: "external" }
+ : { method: "token", token: createRFC8959AccessTokenPlain(newToken) };
if (!newValue.address) newValue.address = {};
if (!newValue.jurisdiction) newValue.jurisdiction = {};
// remove above use conversion
// schema.validateSync(value, { abortEarly: false })
- newValue.default_pay_delay = Duration.toTalerProtocolDuration(newValue.default_pay_delay!) as any
- newValue.default_wire_transfer_delay = Duration.toTalerProtocolDuration(newValue.default_wire_transfer_delay!) as any
+ newValue.default_pay_delay = Duration.toTalerProtocolDuration(
+ newValue.default_pay_delay!,
+ ) as any;
+ newValue.default_wire_transfer_delay = Duration.toTalerProtocolDuration(
+ newValue.default_wire_transfer_delay!,
+ ) as any;
// delete value.default_pay_delay;
// delete value.default_wire_transfer_delay;
- return onCreate(newValue as any as TalerMerchantApi.InstanceConfigurationMessage);
+ return onCreate(
+ newValue as any as TalerMerchantApi.InstanceConfigurationMessage,
+ );
};
function updateToken(token: string | null) {
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
index 431015d6f..b00cfbe7d 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
@@ -19,8 +19,7 @@
*/
import { TalerMerchantApi } from "@gnu-taler/taler-util";
import {
- useMerchantApiContext,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
@@ -39,7 +38,7 @@ export type Entity = TalerMerchantApi.InstanceConfigurationMessage;
export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const { lib } = useMerchantApiContext();
+ const { lib } = useSessionContext();
const { state, logIn } = useSessionContext();
return (
@@ -54,7 +53,7 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
) => {
if (state.status !== "loggedIn") return;
try {
- await lib.management.createInstance(state.token, d);
+ await lib.instance.createInstance(state.token, d);
if (d.auth.token) {
//if auth has been updated, request a new access token
const result = await lib.authenticate.createAccessTokenBearer(
@@ -69,7 +68,7 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
);
if (result.type === "ok") {
const { token } = result.body;
- logIn({ token });
+ logIn(token);
}
}
onConfirm();
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx
index 8166dc739..d4258058b 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx
@@ -40,6 +40,7 @@ function createExample<Props>(
<MerchantApiProviderTesting
value={{
cancelRequest: () => {},
+ changeBackend: () => {},
config: {
currency: "ARS",
version: "1",
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
index a03a2659b..cff3c5a02 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
@@ -20,8 +20,10 @@
*/
import { TalerMerchantApi } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { StateUpdater, useEffect, useState } from "preact/hooks";
import { useSessionContext } from "../../../context/session.js";
@@ -149,7 +151,8 @@ function Table({
onPurge,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
- const { impersonate } = useSessionContext()
+ const { lib } = useSessionContext();
+ const { impersonate } = useSessionContext();
return (
<div class="table-container">
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -198,10 +201,12 @@ function Table({
</td>
<td>
<a
- href={`#/orders?instance=${i.id}`}
- onClick={(e) => {
- impersonate({instance: i.id});
- e.preventDefault();
+ href={`#/orders`}
+ onClick={async (_e) => {
+ // e.preventDefault();
+ const newInstanceApi = lib.subInstanceApi(i.id);
+ //not checking /config since this comes from instance list
+ impersonate(new URL(newInstanceApi.instance.baseUrl));
}}
>
{i.id}
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
index 5b8cf2a5c..5b492e45c 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx
@@ -19,36 +19,29 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useMerchantApiContext,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../components/exception/loading.js";
import { NotificationCard } from "../../../components/menu/index.js";
import { DeleteModal, PurgeModal } from "../../../components/modal/index.js";
+import { useSessionContext } from "../../../context/session.js";
import { useBackendInstances } from "../../../hooks/instance.js";
import { Notification } from "../../../utils/types.js";
+import { LoginPage } from "../../login/index.js";
import { View } from "./View.js";
-import { useSessionContext } from "../../../context/session.js";
interface Props {
onCreate: () => void;
onUpdate: (id: string) => void;
instances: TalerMerchantApi.Instance[];
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
}
export default function Instances({
- onUnauthorized,
- onLoadError,
- onNotFound,
onCreate,
onUpdate,
}: Props): VNode {
@@ -59,29 +52,29 @@ export default function Instances({
useState<TalerMerchantApi.Instance | null>(null);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const { lib } = useMerchantApiContext();
+ const { lib } = useSessionContext();
const { state } = useSessionContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result.case)
+ }
+ }
}
return (
<Fragment>
<NotificationCard notification={notif} />
<View
- instances={result.data.instances}
+ instances={result.body.instances}
onDelete={setDeleting}
onCreate={onCreate}
onPurge={setPurging}
@@ -97,7 +90,7 @@ export default function Instances({
return;
}
try {
- await lib.management.deleteInstance(state.token, deleting.id);
+ await lib.instance.deleteInstance(state.token, deleting.id);
// pushNotification({message: 'delete_success', type: 'SUCCESS' })
setNotif({
message: i18n.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`,
@@ -124,7 +117,7 @@ export default function Instances({
return;
}
try {
- await lib.management.deleteInstance(state.token, purging.id, { purge: true });
+ await lib.instance.deleteInstance(state.token, purging.id, { purge: true });
setNotif({
message: i18n.str`Instance '${purging.name}' (ID: ${purging.id}) has been disabled`,
type: "SUCCESS",
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
index dd77d609c..d05375b6c 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -32,6 +32,7 @@ import { Input } from "../../../../components/form/Input.js";
import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { safeConvertURL } from "../update/UpdatePage.js";
type Entity = TalerMerchantApi.AccountAddDetails & { repeatPassword: string };
@@ -42,62 +43,71 @@ interface Props {
const accountAuthType = ["none", "basic"];
-function isValidURL(s: string): boolean {
- try {
- const u = new URL("/", s)
- return true;
- } catch (e) {
- return false;
- }
-}
-
export function CreatePage({ onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const [state, setState] = useState<Partial<Entity>>({});
+ const facadeURL = safeConvertURL(state.credit_facade_url);
const errors: FormErrors<Entity> = {
payto_uri: !state.payto_uri ? i18n.str`required` : undefined,
credit_facade_credentials: !state.credit_facade_credentials
? undefined
: undefinedIfEmpty({
- username:
- state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username
- ? i18n.str`required`
- : undefined,
- password:
- state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password
- ? i18n.str`required`
- : undefined,
- }),
+ username:
+ state.credit_facade_credentials.type === "basic" &&
+ !state.credit_facade_credentials.username
+ ? i18n.str`required`
+ : undefined,
+ password:
+ state.credit_facade_credentials.type === "basic" &&
+ !state.credit_facade_credentials.password
+ ? i18n.str`required`
+ : undefined,
+ }),
credit_facade_url: !state.credit_facade_url
? undefined
- : !isValidURL(state.credit_facade_url) ? i18n.str`not valid url`
+ : !facadeURL
+ ? i18n.str`Invalid url`
+ : !facadeURL.href.endsWith("/")
+ ? i18n.str`URL should end with a '/'`
+ : facadeURL.searchParams.size > 0
+ ? i18n.str`URL should not contain params`
+ : facadeURL.hash
+ ? i18n.str`URL should not hash param`
+ : undefined,
+ repeatPassword: !state.credit_facade_credentials
+ ? undefined
+ : state.credit_facade_credentials.type === "basic" &&
+ (!state.credit_facade_credentials.password ||
+ state.credit_facade_credentials.password !== state.repeatPassword)
+ ? i18n.str`is not the same`
: undefined,
- repeatPassword:
- !state.credit_facade_credentials
- ? undefined
- : state.credit_facade_credentials.type === "basic" && (!state.credit_facade_credentials.password || state.credit_facade_credentials.password !== state.repeatPassword)
- ? i18n.str`is not the same`
- : undefined,
};
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
);
const submitForm = () => {
if (hasErrors) return Promise.reject();
- const credit_facade_url = !state.credit_facade_url ? undefined : new URL("/", state.credit_facade_url).href
- const credit_facade_credentials: TalerMerchantApi.FacadeCredentials | undefined =
- credit_facade_url == undefined ? undefined :
- state.credit_facade_credentials?.type === "basic" ? {
- type: "basic",
- password: state.credit_facade_credentials.password,
- username: state.credit_facade_credentials.username,
- } : {
- type: "none"
- }
+ const credit_facade_url = !state.credit_facade_url
+ ? undefined
+ : facadeURL?.href;
+ const credit_facade_credentials:
+ | TalerMerchantApi.FacadeCredentials
+ | undefined =
+ credit_facade_url == undefined
+ ? undefined
+ : state.credit_facade_credentials?.type === "basic"
+ ? {
+ type: "basic",
+ password: state.credit_facade_credentials.password,
+ username: state.credit_facade_credentials.username,
+ }
+ : {
+ type: "none",
+ };
return onCreate({
payto_uri: state.payto_uri!,
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
index b12b95f2f..9bab33f6f 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -19,12 +19,25 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerMerchantApi } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import {
+ FacadeCredentials,
+ HttpStatusCode,
+ OperationFail,
+ OperationOk,
+ TalerError,
+ TalerMerchantApi,
+ TalerRevenueHttpClient,
+ assertUnreachable,
+ opFixedSuccess,
+} from "@gnu-taler/taler-util";
+import {
+ BrowserFetchHttpLib,
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useBankAccountAPI } from "../../../../hooks/bank.js";
+import { useSessionContext } from "../../../../context/session.js";
import { Notification } from "../../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
@@ -35,7 +48,8 @@ interface Props {
}
export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
- const { createBankAccount } = useBankAccountAPI();
+ const { lib: api } = useSessionContext();
+ const { state } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -44,10 +58,69 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
<NotificationCard notification={notif} />
<CreatePage
onBack={onBack}
- onCreate={(request: Entity) => {
- return createBankAccount(request)
+ onCreate={async (request: Entity) => {
+ const revenueAPI = !request.credit_facade_url
+ ? undefined
+ : new URL("./", request.credit_facade_url);
+
+ if (revenueAPI) {
+ const resp = await testRevenueAPI(
+ revenueAPI,
+ request.credit_facade_credentials,
+ );
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case TestRevenueErrorType.NO_CONFIG: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.CLIENT_BAD_REQUEST: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Server replied with "bad request".`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.UNAUTHORIZED: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Unauthorized, try with another credentials.`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.NOT_FOUND: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Check facade URL, server replied with "not found".`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.GENERIC_ERROR: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: resp.detail.hint,
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp.case);
+ }
+ }
+ }
+ }
+
+ return api.instance
+ .addBankAccount(state.token, request)
.then(() => {
- onConfirm()
+ onConfirm();
})
.catch((error) => {
setNotif({
@@ -61,3 +134,103 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
</>
);
}
+
+export enum TestRevenueErrorType {
+ NO_CONFIG,
+ CLIENT_BAD_REQUEST,
+ UNAUTHORIZED,
+ NOT_FOUND,
+ GENERIC_ERROR,
+}
+
+export async function testRevenueAPI(
+ revenueAPI: URL,
+ creds: FacadeCredentials | undefined,
+): Promise<OperationOk<void> | OperationFail<TestRevenueErrorType>> {
+ const api = new TalerRevenueHttpClient(
+ revenueAPI.href,
+ new BrowserFetchHttpLib(),
+ );
+ const auth =
+ creds === undefined
+ ? undefined
+ : creds.type === "none"
+ ? undefined
+ : creds.type === "basic"
+ ? {
+ username: creds.username,
+ password: creds.password,
+ }
+ : undefined;
+
+ try {
+ const config = await api.getConfig(auth);
+
+ if (config.type === "fail") {
+ switch (config.case) {
+ case HttpStatusCode.Unauthorized: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.UNAUTHORIZED,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ case HttpStatusCode.NotFound: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.NO_CONFIG,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ }
+ }
+
+ const history = await api.getHistory(auth);
+
+ if (history.type === "fail") {
+ switch (history.case) {
+ case HttpStatusCode.BadRequest: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.CLIENT_BAD_REQUEST,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ case HttpStatusCode.Unauthorized: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.UNAUTHORIZED,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ case HttpStatusCode.NotFound: {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.NOT_FOUND,
+ detail: {
+ code: 1,
+ },
+ };
+ }
+ }
+ }
+ } catch (err) {
+ if (err instanceof TalerError) {
+ return {
+ type: "fail",
+ case: TestRevenueErrorType.GENERIC_ERROR,
+ detail: err.errorDetail,
+ };
+ }
+ }
+
+ return opFixedSuccess(undefined);
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
index 50cf0fe70..4ee68cd80 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
@@ -24,12 +24,12 @@ import { h, VNode } from "preact";
import { CardTable } from "./Table.js";
export interface Props {
- devices: TalerMerchantApi.BankAccountEntry[];
- onLoadMoreBefore?: () => void;
- onLoadMoreAfter?: () => void;
+ devices: TalerMerchantApi.BankAccountSummaryEntry[];
+ // onLoadMoreBefore?: () => void;
+ // onLoadMoreAfter?: () => void;
onCreate: () => void;
- onDelete: (e: TalerMerchantApi.BankAccountEntry) => void;
- onSelect: (e: TalerMerchantApi.BankAccountEntry) => void;
+ onDelete: (e: TalerMerchantApi.BankAccountSummaryEntry) => void;
+ onSelect: (e: TalerMerchantApi.BankAccountSummaryEntry) => void;
}
export function ListPage({
@@ -37,8 +37,8 @@ export function ListPage({
onCreate,
onDelete,
onSelect,
- onLoadMoreBefore,
- onLoadMoreAfter,
+ // onLoadMoreBefore,
+ // onLoadMoreAfter,
}: Props): VNode {
return (
@@ -51,10 +51,10 @@ export function ListPage({
onCreate={onCreate}
onDelete={onDelete}
onSelect={onSelect}
- onLoadMoreBefore={onLoadMoreBefore}
- hasMoreBefore={!onLoadMoreBefore}
- onLoadMoreAfter={onLoadMoreAfter}
- hasMoreAfter={!onLoadMoreAfter}
+ // onLoadMoreBefore={onLoadMoreBefore}
+ // hasMoreBefore={!onLoadMoreBefore}
+ // onLoadMoreAfter={onLoadMoreAfter}
+ // hasMoreAfter={!onLoadMoreAfter}
/>
</section>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
index 690e3a2fc..efe484402 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
@@ -24,17 +24,13 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { StateUpdater, useState } from "preact/hooks";
-type Entity = TalerMerchantApi.BankAccountEntry;
+type Entity = TalerMerchantApi.BankAccountSummaryEntry;
interface Props {
accounts: Entity[];
onDelete: (e: Entity) => void;
onSelect: (e: Entity) => void;
onCreate: () => void;
- onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
- onLoadMoreAfter?: () => void;
}
export function CardTable({
@@ -42,10 +38,6 @@ export function CardTable({
onCreate,
onDelete,
onSelect,
- onLoadMoreAfter,
- onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
@@ -83,10 +75,6 @@ export function CardTable({
onSelect={onSelect}
rowSelection={rowSelection}
rowSelectionHandler={rowSelectionHandler}
- onLoadMoreAfter={onLoadMoreAfter}
- onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
/>
) : (
<EmptyTable />
@@ -103,20 +91,12 @@ interface TableProps {
onDelete: (e: Entity) => void;
onSelect: (e: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
- onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
- onLoadMoreAfter?: () => void;
}
function Table({
accounts,
- onLoadMoreAfter,
onDelete,
onSelect,
- onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
const emptyList: Record<PaytoType | "unknown", { parsed: PaytoUri, acc: Entity }[]> = { "bitcoin": [], "x-taler-bank": [], "iban": [], "unknown": [], }
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
index 72efa08c9..1eda7382d 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
@@ -19,60 +19,59 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useBankAccountAPI, useInstanceBankAccounts } from "../../../../hooks/bank.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { ListPage } from "./ListPage.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
- onNotFound: () => VNode;
onCreate: () => void;
onSelect: (id: string) => void;
}
export default function ListOtpDevices({
- onUnauthorized,
- onLoadError,
onCreate,
onSelect,
- onNotFound,
}: Props): VNode {
- const [position, setPosition] = useState<string | undefined>(undefined);
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { deleteBankAccount } = useBankAccountAPI();
- const result = useInstanceBankAccounts({ position }, (id) => setPosition(id));
+ const { lib: api } = useSessionContext();
+ const { state } = useSessionContext();
+ const result = useInstanceBankAccounts();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
}
return (
<Fragment>
<NotificationCard notification={notif} />
- {result.data.accounts.length < 1 &&
+ {result.body.accounts.length < 1 &&
<NotificationCard notification={{
type: "WARN",
message: i18n.str`You need to associate a bank account to receive revenue.`,
@@ -80,17 +79,17 @@ export default function ListOtpDevices({
}} />
}
<ListPage
- devices={result.data.accounts}
- onLoadMoreBefore={
- result.isReachingStart ? result.loadMorePrev : undefined
- }
- onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ devices={result.body.accounts}
+ // onLoadMoreBefore={
+ // result.isFirstPage ? undefined: result.loadFirst
+ // }
+ // onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
onCreate={onCreate}
onSelect={(e) => {
onSelect(e.h_wire);
}}
- onDelete={(e: TalerMerchantApi.BankAccountEntry) =>
- deleteBankAccount(e.h_wire)
+ onDelete={(e: TalerMerchantApi.BankAccountSummaryEntry) => {
+ return api.instance.deleteBankAccount(state.token, e.h_wire)
.then(() =>
setNotif({
message: i18n.str`bank account delete successfully`,
@@ -105,6 +104,7 @@ export default function ListOtpDevices({
}),
)
}
+ }
/>
</Fragment>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
index 6dd264f29..1a8e9bdc1 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
@@ -33,8 +33,7 @@ import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
-type Entity = TalerMerchantApi.BankAccountEntry
- & WithId;
+type Entity = TalerMerchantApi.BankAccountEntry & WithId;
const accountAuthType = ["unedit", "none", "basic"];
interface Props {
@@ -43,32 +42,56 @@ interface Props {
account: Entity;
}
-
export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const [state, setState] = useState<Partial<TalerMerchantApi.AccountPatchDetails>>(account);
+ const [state, setState] =
+ useState<Partial<TalerMerchantApi.AccountPatchDetails>>(account);
// @ts-expect-error "unedit" is fine since is part of the accountAuthType values
if (state.credit_facade_credentials?.type === "unedit") {
// we use this to set creds to undefined but server don't get this type
- state.credit_facade_credentials = undefined
+ state.credit_facade_credentials = undefined;
}
+ const facadeURL = safeConvertURL(state.credit_facade_url);
+
const errors: FormErrors<TalerMerchantApi.AccountPatchDetails> = {
- credit_facade_url: !state.credit_facade_url ? undefined : !isValidURL(state.credit_facade_url) ? i18n.str`invalid url` : undefined,
+ credit_facade_url: !state.credit_facade_url
+ ? undefined
+ : !facadeURL
+ ? i18n.str`Invalid url`
+ : !facadeURL.href.endsWith("/")
+ ? i18n.str`URL should end with a '/'`
+ : facadeURL.searchParams.size > 0
+ ? i18n.str`URL should not contain params`
+ : facadeURL.hash
+ ? i18n.str`URL should not hash param`
+ : undefined,
credit_facade_credentials: undefinedIfEmpty({
+ username:
+ state.credit_facade_credentials?.type !== "basic"
+ ? undefined
+ : !state.credit_facade_credentials.username
+ ? i18n.str`required`
+ : undefined,
- username: state.credit_facade_credentials?.type !== "basic" ? undefined
- : !state.credit_facade_credentials.username ? i18n.str`required` : undefined,
-
- password: state.credit_facade_credentials?.type !== "basic" ? undefined
- : !state.credit_facade_credentials.password ? i18n.str`required` : undefined,
-
- repeatPassword: state.credit_facade_credentials?.type !== "basic" ? undefined
- : !(state.credit_facade_credentials as any).repeatPassword ? i18n.str`required` :
- (state.credit_facade_credentials as any).repeatPassword !== state.credit_facade_credentials.password ? i18n.str`doesn't match`
+ password:
+ state.credit_facade_credentials?.type !== "basic"
+ ? undefined
+ : !state.credit_facade_credentials.password
+ ? i18n.str`required`
: undefined,
+
+ repeatPassword:
+ state.credit_facade_credentials?.type !== "basic"
+ ? undefined
+ : !(state.credit_facade_credentials as any).repeatPassword
+ ? i18n.str`required`
+ : (state.credit_facade_credentials as any).repeatPassword !==
+ state.credit_facade_credentials.password
+ ? i18n.str`doesn't match`
+ : undefined,
}),
};
@@ -78,18 +101,25 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
const submitForm = () => {
if (hasErrors) return Promise.reject();
-
- const credit_facade_url = !state.credit_facade_url ? undefined : new URL("/", state.credit_facade_url).href
-
- const credit_facade_credentials: TalerMerchantApi.FacadeCredentials | undefined =
- credit_facade_url == undefined || state.credit_facade_credentials === undefined ? undefined :
- state.credit_facade_credentials.type === "basic" ? {
- type: "basic",
- password: state.credit_facade_credentials.password,
- username: state.credit_facade_credentials.username,
- } : {
- type: "none"
- };
+ const credit_facade_url = !state.credit_facade_url
+ ? undefined
+ : facadeURL?.href;
+
+ const credit_facade_credentials:
+ | TalerMerchantApi.FacadeCredentials
+ | undefined =
+ credit_facade_url == undefined ||
+ state.credit_facade_credentials === undefined
+ ? undefined
+ : state.credit_facade_credentials.type === "basic"
+ ? {
+ type: "basic",
+ password: state.credit_facade_credentials.password,
+ username: state.credit_facade_credentials.username,
+ }
+ : {
+ type: "none",
+ };
return onUpdate({ credit_facade_credentials, credit_facade_url });
};
@@ -140,7 +170,7 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
toStr={(str) => {
if (str === "none") return "Without authentication";
if (str === "basic") return "With authentication";
- return "Do not change"
+ return "Do not change";
}}
/>
{state.credit_facade_credentials?.type === "basic" ? (
@@ -191,11 +221,12 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
);
}
-function isValidURL(s: string): boolean {
+//TODO: move to utils
+export function safeConvertURL(s?: string): URL | undefined {
+ if (!s) return undefined;
try {
- const u = new URL("/", s)
- return true;
+ return new URL(s);
} catch (e) {
- return false;
+ return undefined;
}
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
index 742d13b67..70942fd55 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
@@ -19,18 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useBankAccountAPI, useBankAccountDetails } from "../../../../hooks/bank.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useBankAccountDetails } from "../../../../hooks/bank.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
+import { TestRevenueErrorType, testRevenueAPI } from "../create/index.js";
import { UpdatePage } from "./UpdatePage.js";
export type Entity = TalerMerchantApi.AccountPatchDetails & WithId;
@@ -38,48 +41,103 @@ export type Entity = TalerMerchantApi.AccountPatchDetails & WithId;
interface Props {
onBack?: () => void;
onConfirm: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
bid: string;
}
export default function UpdateValidator({
bid,
onConfirm,
onBack,
- onUnauthorized,
- onNotFound,
- onLoadError,
}: Props): VNode {
- const { updateBankAccount } = useBankAccountAPI();
+ const { lib: api } = useSessionContext();
+ const { state } = useSessionContext();
const result = useBankAccountDetails(bid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
return (
<Fragment>
<NotificationCard notification={notif} />
<UpdatePage
- account={{ ...result.data, id: bid }}
+ account={{ ...result.body, id: bid }}
onBack={onBack}
- onUpdate={(data) => {
- return updateBankAccount(bid, data)
+ onUpdate={async (request) => {
+ const revenueAPI = !request.credit_facade_url
+ ? undefined
+ : new URL("./", request.credit_facade_url);
+
+ if (revenueAPI) {
+ const resp = await testRevenueAPI(
+ revenueAPI,
+ request.credit_facade_credentials,
+ );
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case TestRevenueErrorType.NO_CONFIG: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.CLIENT_BAD_REQUEST: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Server replied with "bad request".`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.UNAUTHORIZED: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Unauthorized, try with another credentials.`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.NOT_FOUND: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: i18n.str`Check facade URL, server replied with "not found".`,
+ });
+ return;
+ }
+ case TestRevenueErrorType.GENERIC_ERROR: {
+ setNotif({
+ message: i18n.str`Could not create account`,
+ type: "ERROR",
+ description: resp.detail.hint,
+ });
+ return;
+ }
+ default: {
+ assertUnreachable(resp.case)
+ }
+ }
+ }
+ }
+ return api.instance.updateBankAccount(state.token, bid, request)
.then(onConfirm)
.catch((error) => {
setNotif({
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
index 2714c8e02..e1a7f87f0 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
@@ -13,70 +13,70 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ErrorType, HttpError, useMerchantApiContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../components/exception/loading.js";
import { DeleteModal } from "../../../components/modal/index.js";
+import { useSessionContext } from "../../../context/session.js";
import { useInstanceDetails } from "../../../hooks/instance.js";
+import { LoginPage } from "../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../notfound/index.js";
import { DetailPage } from "./DetailPage.js";
-import { HttpStatusCode, TalerErrorDetail } from "@gnu-taler/taler-util";
-import { useSessionContext } from "../../../context/session.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
onUpdate: () => void;
- onNotFound: () => VNode;
onDelete: () => void;
}
export default function Detail({
onUpdate,
- onLoadError,
- onUnauthorized,
onDelete,
- onNotFound,
}: Props): VNode {
const { state } = useSessionContext();
const result = useInstanceDetails();
const [deleting, setDeleting] = useState<boolean>(false);
// const { deleteInstance } = useInstanceAPI();
- const { lib } = useMerchantApiContext();
+ const { lib } = useSessionContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
}
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
+ }
+
return (
<Fragment>
<DetailPage
- selected={result.data}
+ selected={result.body}
onUpdate={onUpdate}
onDelete={() => setDeleting(true)}
/>
{deleting && (
<DeleteModal
- element={{ name: result.data.name, id: state.instance }}
+ element={{ name: result.body.name, id: state.instance }}
onCancel={() => setDeleting(false)}
onConfirm={async (): Promise<void> => {
if (state.status !== "loggedIn") {
return
}
try {
- await lib.management.deleteCurrentInstance(state.token);
+ await lib.instance.deleteCurrentInstance(state.token);
onDelete();
} catch (error) {
//FIXME: show message error
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
index 6cd2d9491..42cb1cb02 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
@@ -39,7 +39,8 @@ function createExample<Props>(
const component = (args: any) => (
<MerchantApiProviderTesting
value={{
- cancelRequest: () => {},
+ cancelRequest: () => { },
+ changeBackend: () => { },
config: {
currency: "ARS",
version: "1",
@@ -57,7 +58,7 @@ function createExample<Props>(
},
hints: [],
lib: {} as any,
- onActivity: (() => {}) as any,
+ onActivity: (() => { }) as any,
url: new URL("asdasd"),
}}
>
@@ -70,7 +71,7 @@ function createExample<Props>(
export const Example = createExample(TestedComponent, {
selected: {
name: "name",
- auth: { type: "external" },
+ auth: { method: "external" },
address: {},
user_type: "business",
jurisdiction: {},
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
index 555eb47b9..ed0e1220f 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx
@@ -19,41 +19,55 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { useInstanceKYCDetails } from "../../../../hooks/instance.js";
import { ListPage } from "./ListPage.js";
-import { HttpStatusCode, TalerErrorDetail } from "@gnu-taler/taler-util";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
- onNotFound: () => VNode;
}
-export default function ListKYC({
- onUnauthorized,
- onLoadError,
- onNotFound,
-}: Props): VNode {
+export default function ListKYC(_p: Props): VNode {
const result = useInstanceKYCDetails();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
}
+ /**
+ * This component just render known kyc requirements.
+ * If query fail then is safe to hide errors.
+ */
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.GatewayTimeout: {
+ return <div />
+ }
+ case HttpStatusCode.BadGateway: {
+ const status = result.body;
- const status = result.data.type === "ok" ? undefined : result.data.status;
+ if (!status) {
+ return <div>no kyc required</div>;
+ }
+ return <ListPage status={status} />;
+
+ }
+ case HttpStatusCode.ServiceUnavailable: {
+ return <div />
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <div />
+ }
+ case HttpStatusCode.NotFound: {
+ return <div />;
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
+ }
+ const status = result.body;
if (!status) {
return <div>no kyc required</div>;
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
index 041ec73e7..7be3d23f6 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
@@ -28,8 +28,7 @@ import {
TalerProtocolDuration,
} from "@gnu-taler/taler-util";
import {
- useMerchantApiContext,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { format, isFuture } from "date-fns";
import { Fragment, VNode, h } from "preact";
@@ -49,6 +48,7 @@ import { InputToggle } from "../../../../components/form/InputToggle.js";
import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js";
import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js";
import { ProductList } from "../../../../components/product/ProductList.js";
+import { useSessionContext } from "../../../../context/session.js";
import { usePreference } from "../../../../hooks/preference.js";
import { rate } from "../../../../utils/amount.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
@@ -134,7 +134,7 @@ export function CreatePage({
instanceConfig,
instanceInventory,
}: Props): VNode {
- const { config } = useMerchantApiContext();
+ const { config } = useSessionContext();
const instance_default = with_defaults(instanceConfig, config.currency);
const [value, valueHandler] = useState(instance_default);
const zero = Amounts.zeroOfCurrency(config.currency);
@@ -679,7 +679,6 @@ export function CreatePage({
value.extra &&
value.extra[key] !== undefined
) {
- console.log(value.extra);
delete value.extra[key];
}
valueHandler({
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
index 3f7b20f52..32f3f05c7 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
@@ -13,12 +13,15 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
+import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
+import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully.js";
-import { useOrderAPI } from "../../../../hooks/order.js";
+import { useOrderDetails } from "../../../../hooks/order.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { Entity } from "./index.js";
+import { LoginPage } from "../../../login/index.js";
interface Props {
entity: Entity;
@@ -31,14 +34,38 @@ export function OrderCreatedSuccessfully({
onConfirm,
onCreateAnother,
}: Props): VNode {
- const { getPaymentURL } = useOrderAPI();
- const [url, setURL] = useState<string | undefined>(undefined);
+ const result = useOrderDetails(entity.response.order_id)
const { i18n } = useTranslationContext();
- useEffect(() => {
- getPaymentURL(entity.response.order_id).then((response) => {
- setURL(response.data);
- });
- }, [getPaymentURL, entity.response.order_id]);
+
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />
+ }
+ case HttpStatusCode.BadGateway: {
+ return <div>Failed to obtain a response from the exchange</div>;
+ }
+ case HttpStatusCode.GatewayTimeout: {
+ return (
+ <div>The merchant's interaction with the exchange took too long</div>
+ );
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
+ }
+
+ const url = result.body.order_status === "unpaid" ?
+ result.body.taler_pay_uri :
+ result.body.contract_terms.fulfillment_url
return (
<CreatedSuccessfully
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx
index 0f8618435..861114014 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx
@@ -19,16 +19,18 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
-import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
import { useInstanceDetails } from "../../../../hooks/instance.js";
-import { useOrderAPI } from "../../../../hooks/order.js";
import { useInstanceProducts } from "../../../../hooks/product.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { CreatePage } from "./CreatePage.js";
export type Entity = {
@@ -38,52 +40,50 @@ export type Entity = {
interface Props {
onBack?: () => void;
onConfirm: (id: string) => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
}
export default function OrderCreate({
onConfirm,
onBack,
- onLoadError,
- onNotFound,
- onUnauthorized,
}: Props): VNode {
- const { createOrder } = useOrderAPI();
+ const { lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
-
+ const { state } = useSessionContext();
const detailsResult = useInstanceDetails();
const inventoryResult = useInstanceProducts();
- if (detailsResult.loading) return <Loading />;
- if (inventoryResult.loading) return <Loading />;
-
- if (!detailsResult.ok) {
- if (
- detailsResult.type === ErrorType.CLIENT &&
- detailsResult.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- detailsResult.type === ErrorType.CLIENT &&
- detailsResult.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(detailsResult);
+ if (!detailsResult) return <Loading />
+ if (detailsResult instanceof TalerError) {
+ return <ErrorLoadingMerchant error={detailsResult} />
}
-
- if (!inventoryResult.ok) {
- if (
- inventoryResult.type === ErrorType.CLIENT &&
- inventoryResult.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- inventoryResult.type === ErrorType.CLIENT &&
- inventoryResult.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(inventoryResult);
+ if (detailsResult.type === "fail") {
+ switch (detailsResult.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ default: {
+ assertUnreachable(detailsResult);
+ }
+ }
+ }
+ if (!inventoryResult) return <Loading />
+ if (inventoryResult instanceof TalerError) {
+ return <ErrorLoadingMerchant error={inventoryResult} />
+ }
+ if (inventoryResult.type === "fail") {
+ switch (inventoryResult.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(inventoryResult);
+ }
+ }
}
return (
@@ -93,9 +93,16 @@ export default function OrderCreate({
<CreatePage
onBack={onBack}
onCreate={(request: TalerMerchantApi.PostOrderRequest) => {
- createOrder(request)
+ lib.instance.createOrder(state.token, request)
.then((r) => {
- return onConfirm(r.data.order_id)
+ if (r.type === "ok") {
+ return onConfirm(r.body.order_id)
+ } else {
+ setNotif({
+ message: "could not create order",
+ type: "ERROR",
+ });
+ }
})
.catch((error) => {
setNotif({
@@ -105,8 +112,8 @@ export default function OrderCreate({
});
});
}}
- instanceConfig={detailsResult.data}
- instanceInventory={inventoryResult.data}
+ instanceConfig={detailsResult.body}
+ instanceInventory={inventoryResult.body}
/>
</Fragment>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
index 4ed78b002..498ea83e3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
@@ -25,7 +25,9 @@ import {
TalerMerchantApi,
stringifyRefundUri,
} from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
import { format, formatDistance } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
@@ -158,6 +160,10 @@ function ClaimedPage({
id: string;
order: TalerMerchantApi.CheckPaymentClaimedResponse;
}) {
+ const now = new Date();
+ const refundable =
+ order.contract_terms.refund_deadline.t_s !== "never" &&
+ now.getTime() < order.contract_terms.refund_deadline.t_s * 1000;
const events: Event[] = [];
if (order.contract_terms.timestamp.t_s !== "never") {
events.push({
@@ -173,20 +179,20 @@ function ClaimedPage({
type: "deadline",
});
}
- if (order.contract_terms.refund_deadline.t_s !== "never") {
+ if (order.contract_terms.refund_deadline.t_s !== "never" && refundable) {
events.push({
when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
description: "refund deadline",
type: "deadline",
});
}
- if (order.contract_terms.wire_transfer_deadline.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000),
- description: "wire deadline",
- type: "deadline",
- });
- }
+ // if (order.contract_terms.wire_transfer_deadline.t_s !== "never") {
+ // events.push({
+ // when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000),
+ // description: "wire deadline",
+ // type: "deadline",
+ // });
+ // }
if (
order.contract_terms.delivery_date &&
order.contract_terms.delivery_date.t_s !== "never"
@@ -325,22 +331,13 @@ function PaidPage({
order: TalerMerchantApi.CheckPaymentPaidResponse;
onRefund: (id: string) => void;
}) {
+ const now = new Date();
+ const refundable =
+ order.contract_terms.refund_deadline.t_s !== "never" &&
+ now.getTime() < order.contract_terms.refund_deadline.t_s * 1000;
+
const events: Event[] = [];
- if (order.contract_terms.timestamp.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.timestamp.t_s * 1000),
- description: "order created",
- type: "start",
- });
- }
- if (order.contract_terms.pay_deadline.t_s !== "never") {
- events.push({
- when: new Date(order.contract_terms.pay_deadline.t_s * 1000),
- description: "pay deadline",
- type: "deadline",
- });
- }
- if (order.contract_terms.refund_deadline.t_s !== "never") {
+ if (order.contract_terms.refund_deadline.t_s !== "never" && refundable) {
events.push({
when: new Date(order.contract_terms.refund_deadline.t_s * 1000),
description: "refund deadline",
@@ -374,70 +371,71 @@ function PaidPage({
});
}
});
- if (order.wire_details && order.wire_details.length) {
- if (order.wire_details.length > 1) {
- let last: TalerMerchantApi.TransactionWireTransfer | null = null;
- let first: TalerMerchantApi.TransactionWireTransfer | null = null;
- let total: AmountJson | null = null;
-
- order.wire_details.forEach((w) => {
- if (last === null || last.execution_time.t_s < w.execution_time.t_s) {
- last = w;
- }
- if (first === null || first.execution_time.t_s > w.execution_time.t_s) {
- first = w;
- }
- total =
- total === null
- ? Amounts.parseOrThrow(w.amount)
- : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount;
- });
- const last_time = last!.execution_time.t_s;
- if (last_time !== "never") {
- events.push({
- when: new Date(last_time * 1000),
- description: `wired ${Amounts.stringify(total!)}`,
- type: "wired-range",
- });
- }
- const first_time = first!.execution_time.t_s;
- if (first_time !== "never") {
- events.push({
- when: new Date(first_time * 1000),
- description: `wire transfer started...`,
- type: "wired-range",
+ const ra = !order.refunded ? undefined : Amounts.parse(order.refund_amount);
+ const am = Amounts.parseOrThrow(order.contract_terms.amount);
+ if (ra && Amounts.cmp(ra, am) === 1) {
+ if (order.wire_details && order.wire_details.length) {
+ if (order.wire_details.length > 1) {
+ let last: TalerMerchantApi.TransactionWireTransfer | null = null;
+ let first: TalerMerchantApi.TransactionWireTransfer | null = null;
+ let total: AmountJson | null = null;
+
+ order.wire_details.forEach((w) => {
+ if (last === null || last.execution_time.t_s < w.execution_time.t_s) {
+ last = w;
+ }
+ if (
+ first === null ||
+ first.execution_time.t_s > w.execution_time.t_s
+ ) {
+ first = w;
+ }
+ total =
+ total === null
+ ? Amounts.parseOrThrow(w.amount)
+ : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount;
});
- }
- } else {
- order.wire_details.forEach((e) => {
- if (e.execution_time.t_s !== "never") {
+ const last_time = last!.execution_time.t_s;
+ if (last_time !== "never") {
events.push({
- when: new Date(e.execution_time.t_s * 1000),
- description: `wired ${e.amount}`,
- type: "wired",
+ when: new Date(last_time * 1000),
+ description: `wired ${Amounts.stringify(total!)}`,
+ type: "wired-range",
});
}
- });
+ const first_time = first!.execution_time.t_s;
+ if (first_time !== "never") {
+ events.push({
+ when: new Date(first_time * 1000),
+ description: `wire transfer started...`,
+ type: "wired-range",
+ });
+ }
+ } else {
+ order.wire_details.forEach((e) => {
+ if (e.execution_time.t_s !== "never") {
+ events.push({
+ when: new Date(e.execution_time.t_s * 1000),
+ description: `wired ${e.amount}`,
+ type: "wired",
+ });
+ }
+ });
+ }
}
}
- const now = new Date();
const nextEvent = events.find((e) => {
return e.when.getTime() > now.getTime();
});
const [value, valueHandler] = useState<Partial<Paid>>(order);
- const {
- state: { backendUrl },
- } = useSessionContext();
+ const { state } = useSessionContext();
const refundurl = stringifyRefundUri({
- merchantBaseUrl: backendUrl,
+ merchantBaseUrl: state.backendUrl.href,
orderId: order.contract_terms.order_id,
});
- const refundable =
- order.contract_terms.refund_deadline.t_s !== "never" &&
- new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000;
const { i18n } = useTranslationContext();
const amount = Amounts.parseOrThrow(order.contract_terms.amount);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
index a7fe1801b..b28e59b29 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx
@@ -13,55 +13,63 @@
You should have received a 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, TalerErrorDetail } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useOrderDetails } from "../../../../hooks/order.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { DetailPage } from "./DetailPage.js";
export interface Props {
oid: string;
-
onBack: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
}
-export default function Update({
- oid,
- onBack,
- onLoadError,
- onNotFound,
- onUnauthorized,
-}: Props): VNode {
- const { refundOrder } = useOrderAPI();
+export default function Update({ oid, onBack }: Props): VNode {
const result = useOrderDetails(oid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { lib: api } = useSessionContext();
+ const { state } = useSessionContext();
const { i18n } = useTranslationContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.BadGateway: {
+ return <div>Failed to obtain a response from the exchange</div>;
+ }
+ case HttpStatusCode.GatewayTimeout: {
+ return (
+ <div>The merchant's interaction with the exchange took too long</div>
+ );
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
return (
@@ -71,8 +79,12 @@ export default function Update({
<DetailPage
onBack={onBack}
id={oid}
- onRefund={(id, value) =>
- refundOrder(id, value)
+ onRefund={(id, value) => {
+ if (state.status !== "loggedIn") {
+ return;
+ }
+ api.instance
+ .addRefund(state.token, id, value)
.then(() =>
setNotif({
message: i18n.str`refund created successfully`,
@@ -85,9 +97,9 @@ export default function Update({
type: "ERROR",
description: error.message,
}),
- )
- }
- selected={result.data}
+ );
+ }}
+ selected={result.body}
/>
</Fragment>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
index 7b88985dc..408bc0c0a 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerMerchantApi } from "@gnu-taler/taler-util";
+import { AbsoluteTime, TalerMerchantApi } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
@@ -43,13 +43,11 @@ export interface ListPageProps {
isNotWiredActive: string;
isWiredActive: string;
- jumpToDate?: Date;
- onSelectDate: (date?: Date) => void;
+ jumpToDate?: AbsoluteTime;
+ onSelectDate: (date?: AbsoluteTime) => void;
orders: (TalerMerchantApi.OrderHistoryEntry & WithId)[];
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
onSelectOrder: (o: TalerMerchantApi.OrderHistoryEntry & WithId) => void;
@@ -58,8 +56,6 @@ export interface ListPageProps {
}
export function ListPage({
- hasMoreAfter,
- hasMoreBefore,
onLoadMoreAfter,
onLoadMoreBefore,
orders,
@@ -177,7 +173,7 @@ export function ListPage({
class="input"
type="text"
readonly
- value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))}
+ value={!jumpToDate || jumpToDate.t_ms === "never" ? "" : format(jumpToDate.t_ms, dateFormatForSettings(settings))}
placeholder={i18n.str`date (${dateFormatForSettings(settings)})`}
onClick={() => {
setPickDate(true);
@@ -207,7 +203,9 @@ export function ListPage({
<DatePicker
opened={pickDate}
closeFunction={() => setPickDate(false)}
- dateReceiver={onSelectDate}
+ dateReceiver={(d) => {
+ onSelectDate(AbsoluteTime.fromMilliseconds(d.getTime()))
+ }}
/>
<CardTable
@@ -216,8 +214,6 @@ export function ListPage({
onCopyURL={onCopyURL}
onSelect={onSelectOrder}
onRefund={onRefundOrder}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
/>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx
index c3df81b87..5ece34409 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx
@@ -21,8 +21,7 @@
import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util";
import {
- useMerchantApiContext,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { VNode, h } from "preact";
@@ -36,6 +35,7 @@ import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputGroup } from "../../../../components/form/InputGroup.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
import { ConfirmModal } from "../../../../components/modal/index.js";
+import { useSessionContext } from "../../../../context/session.js";
import {
datetimeFormatForSettings,
usePreference,
@@ -50,8 +50,6 @@ interface Props {
onCreate: () => void;
onSelect: (order: Entity) => void;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -63,8 +61,6 @@ export function CardTable({
onSelect,
onLoadMoreAfter,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
@@ -105,8 +101,6 @@ export function CardTable({
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
/>
) : (
<EmptyTable />
@@ -125,8 +119,6 @@ interface TableProps {
onSelect: (id: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -137,16 +129,14 @@ function Table({
onCopyURL,
onLoadMoreAfter,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
const [settings] = usePreference();
return (
<div class="table-container">
- {hasMoreBefore && (
+ {onLoadMoreBefore && (
<button class="button is-fullwidth" onClick={onLoadMoreBefore}>
- <i18n.Translate>load newer orders</i18n.Translate>
+ <i18n.Translate>load first page</i18n.Translate>
</button>
)}
<table class="table is-striped is-hoverable is-fullwidth">
@@ -218,9 +208,11 @@ function Table({
})}
</tbody>
</table>
- {hasMoreAfter && (
- <button class="button is-fullwidth" onClick={onLoadMoreAfter}>
- <i18n.Translate>load older orders</i18n.Translate>
+ {onLoadMoreAfter && (
+ <button class="button is-fullwidth"
+ data-tooltip={i18n.str`load more orders after the last one`}
+ onClick={onLoadMoreAfter}>
+ <i18n.Translate>load next page</i18n.Translate>
</button>
)}
</div>
@@ -266,7 +258,7 @@ export function RefundModal({
order.order_status === "paid" ? order.refund_details : []
).reduce(mergeRefunds, []);
- const { config } = useMerchantApiContext();
+ const { config } = useSessionContext();
const totalRefunded = refunds
.map((r) => r.amount)
.reduce(
@@ -301,7 +293,7 @@ export function RefundModal({
: undefined,
};
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
);
const validateAndConfirm = () => {
@@ -380,7 +372,7 @@ export function RefundModal({
<FormProvider<State>
errors={errors}
object={form}
- valueHandler={(d) => setValue(d as any)}
+ valueHandler={(d) => setValue(d)}
>
<InputCurrency<State>
name="refund"
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
index cd62685ca..8a1f85b1c 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx
@@ -19,81 +19,87 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ AbsoluteTime,
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
import {
InstanceOrderFilter,
useInstanceOrders,
- useOrderAPI,
useOrderDetails,
} from "../../../../hooks/order.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { ListPage } from "./ListPage.js";
import { RefundModal } from "./Table.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
- onNotFound: () => VNode;
onSelect: (id: string) => void;
onCreate: () => void;
}
-export default function OrderList({
- onUnauthorized,
- onLoadError,
- onCreate,
- onSelect,
- onNotFound,
-}: Props): VNode {
- const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: "no" });
+export default function OrderList({ onCreate, onSelect }: Props): VNode {
+ const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: false });
const [orderToBeRefunded, setOrderToBeRefunded] = useState<
TalerMerchantApi.OrderHistoryEntry | undefined
>(undefined);
- const setNewDate = (date?: Date): void =>
+ const setNewDate = (date?: AbsoluteTime): void =>
setFilter((prev) => ({ ...prev, date }));
- const result = useInstanceOrders(filter, setNewDate);
- const { refundOrder, getPaymentURL } = useOrderAPI();
+ const result = useInstanceOrders(filter, (d) =>
+ setFilter({ ...filter, position: d }),
+ );
+ const { lib } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
+ const { state } = useSessionContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
}
- const isNotPaidActive = filter.paid === "no" ? "is-active" : "";
- const isPaidActive = filter.paid === "yes" && filter.wired === undefined ? "is-active" : "";
- const isRefundedActive = filter.refunded === "yes" ? "is-active" : "";
- const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : "";
- const isWiredActive = filter.wired === "yes" ? "is-active" : "";
+ const isNotPaidActive = filter.paid === false ? "is-active" : "";
+ const isPaidActive =
+ filter.paid === true && filter.wired === undefined ? "is-active" : "";
+ const isRefundedActive = filter.refunded === true ? "is-active" : "";
+ const isNotWiredActive =
+ filter.wired === false && filter.paid === true ? "is-active" : "";
+ const isWiredActive = filter.wired === true ? "is-active" : "";
const isAllActive =
filter.paid === undefined &&
- filter.refunded === undefined &&
- filter.wired === undefined
+ filter.refunded === undefined &&
+ filter.wired === undefined
? "is-active"
: "";
@@ -102,18 +108,19 @@ export default function OrderList({
<NotificationCard notification={notif} />
<JumpToElementById
- testIfExist={getPaymentURL}
+ testIfExist={async (order) => {
+ const resp = await lib.instance.getOrderDetails(state.token, order);
+ return resp.type === "ok";
+ }}
onSelect={onSelect}
description={i18n.str`jump to order with the given product ID`}
placeholder={i18n.str`order id`}
/>
<ListPage
- orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))}
- onLoadMoreBefore={result.loadMorePrev}
- hasMoreBefore={!result.isReachingStart}
- onLoadMoreAfter={result.loadMore}
- hasMoreAfter={!result.isReachingEnd}
+ orders={result.body.map((o) => ({ ...o, id: o.order_id }))}
+ onLoadMoreBefore={result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
onSelectOrder={(order) => onSelect(order.id)}
onRefundOrder={(value) => setOrderToBeRefunded(value)}
isAllActive={isAllActive}
@@ -123,25 +130,36 @@ export default function OrderList({
isNotPaidActive={isNotPaidActive}
isRefundedActive={isRefundedActive}
jumpToDate={filter.date}
- onCopyURL={(id) =>
- getPaymentURL(id).then((resp) => copyToClipboard(resp.data))
- }
- onCreate={onCreate}
onSelectDate={setNewDate}
+ onCopyURL={async (id) => {
+ const resp = await lib.instance.getOrderDetails(state.token, id);
+ if (resp.type === "ok") {
+ if (resp.body.order_status === "unpaid") {
+ copyToClipboard(resp.body.taler_pay_uri);
+ } else {
+ if (resp.body.contract_terms.fulfillment_url) {
+ copyToClipboard(resp.body.contract_terms.fulfillment_url);
+ }
+ }
+ copyToClipboard(resp.body.order_status);
+ }
+ }}
+ onCreate={onCreate}
onShowAll={() => setFilter({})}
- onShowNotPaid={() => setFilter({ paid: "no" })}
- onShowPaid={() => setFilter({ paid: "yes" })}
- onShowRefunded={() => setFilter({ refunded: "yes" })}
- onShowNotWired={() => setFilter({ wired: "no", paid: "yes" })}
- onShowWired={() => setFilter({ wired: "yes" })}
+ onShowNotPaid={() => setFilter({ paid: false })}
+ onShowPaid={() => setFilter({ paid: true })}
+ onShowRefunded={() => setFilter({ refunded: true })}
+ onShowNotWired={() => setFilter({ wired: false, paid: true })}
+ onShowWired={() => setFilter({ wired: true })}
/>
{orderToBeRefunded && (
<RefundModalForTable
id={orderToBeRefunded.order_id}
onCancel={() => setOrderToBeRefunded(undefined)}
- onConfirm={(value) =>
- refundOrder(orderToBeRefunded.order_id, value)
+ onConfirm={(value) => {
+ lib.instance
+ .addRefund(state.token, orderToBeRefunded.order_id, value)
.then(() =>
setNotif({
message: i18n.str`refund created successfully`,
@@ -155,26 +173,7 @@ export default function OrderList({
description: error.message,
}),
)
- .then(() => setOrderToBeRefunded(undefined))
- }
- onLoadError={(error) => {
- setNotif({
- message: i18n.str`could not create the refund`,
- type: "ERROR",
- description: error.message,
- });
- setOrderToBeRefunded(undefined);
- return <div />;
- }}
- onUnauthorized={onUnauthorized}
- onNotFound={() => {
- setNotif({
- message: i18n.str`could not get the order to refund`,
- type: "ERROR",
- // description: error.message
- });
- setOrderToBeRefunded(undefined);
- return <div />;
+ .then(() => setOrderToBeRefunded(undefined));
}}
/>
)}
@@ -184,41 +183,42 @@ export default function OrderList({
interface RefundProps {
id: string;
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
- onNotFound: () => VNode;
onCancel: () => void;
onConfirm: (m: TalerMerchantApi.RefundRequest) => void;
}
-function RefundModalForTable({
- id,
- onUnauthorized,
- onLoadError,
- onNotFound,
- onConfirm,
- onCancel,
-}: RefundProps): VNode {
+function RefundModalForTable({ id, onConfirm, onCancel }: RefundProps): VNode {
const result = useOrderDetails(id);
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.BadGateway: {
+ return <div>Failed to obtain a response from the exchange</div>;
+ }
+ case HttpStatusCode.GatewayTimeout: {
+ return (
+ <div>The merchant's interaction with the exchange took too long</div>
+ );
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
return (
<RefundModal
- order={result.data}
+ order={result.body}
onCancel={onCancel}
onConfirm={onConfirm}
/>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
index b1b4a0cf7..7723bec81 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
@@ -33,11 +33,8 @@ export function CreatedSuccessfully({
onConfirm,
}: Props): VNode {
const { i18n } = useTranslationContext();
- const {
- state: { backendUrl },
- } = useSessionContext();
const { state } = useSessionContext();
- const issuer = backendUrl;
+ const issuer = state.backendUrl.href;
const qrText = `otpauth://totp/${state.instance}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`;
const qrTextSafe = `otpauth://totp/${state.instance}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`;
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
index e4501a053..8ab0e1f26 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx
@@ -21,13 +21,13 @@
import { TalerMerchantApi } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { useSessionContext } from "../../../../context/session.js";
import { Notification } from "../../../../utils/types.js";
-import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
import { CreatePage } from "./CreatePage.js";
+import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
export type Entity = TalerMerchantApi.OtpDeviceAddDetails;
interface Props {
@@ -36,7 +36,8 @@ interface Props {
}
export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
- const { createOtpDevice } = useOtpDeviceAPI();
+ const { lib: api } = useSessionContext();
+ const { state } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
const [created, setCreated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null)
@@ -51,7 +52,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
<CreatePage
onBack={onBack}
onCreate={(request: Entity) => {
- return createOtpDevice(request)
+ return api.instance.addOtpDevice(state.token, request)
.then((d) => {
setCreated(request)
})
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
index 9022cc35b..8ca0a9c58 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx
@@ -52,9 +52,7 @@ export function ListPage({
onDelete={onDelete}
onSelect={onSelect}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreBefore={!onLoadMoreBefore}
onLoadMoreAfter={onLoadMoreAfter}
- hasMoreAfter={!onLoadMoreAfter}
/>
</section>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
index 7b1ccd4fc..afe3c98e2 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx
@@ -32,8 +32,6 @@ interface Props {
onSelect: (e: Entity) => void;
onCreate: () => void;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -44,8 +42,6 @@ export function CardTable({
onSelect,
onLoadMoreAfter,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
@@ -85,8 +81,6 @@ export function CardTable({
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
/>
) : (
<EmptyTable />
@@ -104,8 +98,6 @@ interface TableProps {
onSelect: (e: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -115,19 +107,17 @@ function Table({
onDelete,
onSelect,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
return (
<div class="table-container">
- {hasMoreBefore && (
+ {onLoadMoreBefore && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more devices before the first one`}
onClick={onLoadMoreBefore}
>
- <i18n.Translate>load newer devices</i18n.Translate>
+ <i18n.Translate>load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -174,13 +164,13 @@ function Table({
})}
</tbody>
</table>
- {hasMoreAfter && (
+ {onLoadMoreAfter && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more devices after the last one`}
onClick={onLoadMoreAfter}
>
- <i18n.Translate>load older devices</i18n.Translate>
+ <i18n.Translate>load next page</i18n.Translate>
</button>
)}
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
index 7fd827956..b6a077863 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx
@@ -19,54 +19,56 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { ListPage } from "./ListPage.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
- onNotFound: () => VNode;
onCreate: () => void;
onSelect: (id: string) => void;
}
-export default function ListOtpDevices({
- onUnauthorized,
- onLoadError,
- onCreate,
- onSelect,
- onNotFound,
-}: Props): VNode {
- const [position, setPosition] = useState<string | undefined>(undefined);
+export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode {
+ // const [position, setPosition] = useState<string | undefined>(undefined);
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { deleteOtpDevice } = useOtpDeviceAPI();
- const result = useInstanceOtpDevices({ position }, (id) => setPosition(id));
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+ const result = useInstanceOtpDevices();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
return (
@@ -74,17 +76,16 @@ export default function ListOtpDevices({
<NotificationCard notification={notif} />
<ListPage
- devices={result.data.otp_devices}
- onLoadMoreBefore={
- result.isReachingStart ? result.loadMorePrev : undefined
- }
- onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ devices={result.body.otp_devices}
+ onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext}
onCreate={onCreate}
onSelect={(e) => {
onSelect(e.otp_device_id);
}}
- onDelete={(e: TalerMerchantApi.OtpDeviceEntry) =>
- deleteOtpDevice(e.otp_device_id)
+ onDelete={(e: TalerMerchantApi.OtpDeviceEntry) => {
+ return lib.instance
+ .deleteOtpDevice(state.token, e.otp_device_id)
.then(() =>
setNotif({
message: i18n.str`validator delete successfully`,
@@ -97,8 +98,8 @@ export default function ListOtpDevices({
type: "ERROR",
description: error.message,
}),
- )
- }
+ );
+ }}
/>
</Fragment>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
index a824c6936..99edb95c3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx
@@ -19,18 +19,25 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useOtpDeviceAPI, useOtpDeviceDetails } from "../../../../hooks/otp.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useOtpDeviceDetails } from "../../../../hooks/otp.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { CreatedSuccessfully } from "../create/CreatedSuccessfully.js";
import { UpdatePage } from "./UpdatePage.js";
@@ -39,43 +46,42 @@ export type Entity = TalerMerchantApi.OtpDevicePatchDetails & WithId;
interface Props {
onBack?: () => void;
onConfirm: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
vid: string;
}
export default function UpdateValidator({
vid,
onConfirm,
onBack,
- onUnauthorized,
- onNotFound,
- onLoadError,
}: Props): VNode {
- const { updateOtpDevice } = useOtpDeviceAPI();
const result = useOtpDeviceDetails(vid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const [keyUpdated, setKeyUpdated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null)
+ const [keyUpdated, setKeyUpdated] =
+ useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null);
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
const { i18n } = useTranslationContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
if (keyUpdated) {
- return <CreatedSuccessfully entity={keyUpdated} onConfirm={onConfirm} />
+ return <CreatedSuccessfully entity={keyUpdated} onConfirm={onConfirm} />;
}
return (
@@ -84,25 +90,47 @@ export default function UpdateValidator({
<UpdatePage
device={{
id: vid,
- otp_algorithm: result.data.otp_algorithm,
- otp_device_description: result.data.device_description,
+ otp_algorithm: result.body.otp_algorithm,
+ otp_device_description: result.body.device_description,
otp_key: "",
- otp_ctr: result.data.otp_ctr
+ otp_ctr: result.body.otp_ctr,
}}
onBack={onBack}
onUpdate={async (newInfo) => {
- return updateOtpDevice(vid, newInfo)
+ return lib.instance
+ .updateOtpDevice(state.token, vid, newInfo)
.then((d) => {
- if (newInfo.otp_key) {
- setKeyUpdated({
- otp_algorithm: newInfo.otp_algorithm,
- otp_device_description: newInfo.otp_device_description,
- otp_device_id: newInfo.id,
- otp_key: newInfo.otp_key,
- otp_ctr: newInfo.otp_ctr,
- })
+ if (d.type === "ok") {
+ if (newInfo.otp_key) {
+ setKeyUpdated({
+ otp_algorithm: newInfo.otp_algorithm,
+ otp_device_description: newInfo.otp_device_description,
+ otp_device_id: newInfo.id,
+ otp_key: newInfo.otp_key,
+ otp_ctr: newInfo.otp_ctr,
+ });
+ } else {
+ onConfirm();
+ }
} else {
- onConfirm()
+ switch(d.case) {
+ case HttpStatusCode.NotFound: {
+ setNotif({
+ message: i18n.str`Could not update template`,
+ type: "ERROR",
+ description: i18n.str`Template id is unknown`,
+ });
+ break;
+ }
+ case HttpStatusCode.Conflict: {
+ setNotif({
+ message: i18n.str`Could not update template`,
+ type: "ERROR",
+ description: i18n.str`The provided information is inconsistent with the current state of the template`,
+ });
+ break;
+ }
+ }
}
})
.catch((error) => {
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx
index 9935a9625..9de5cae78 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx
@@ -21,10 +21,10 @@
import { TalerMerchantApi } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useProductAPI } from "../../../../hooks/product.js";
+import { useSessionContext } from "../../../../context/session.js";
import { Notification } from "../../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
@@ -34,7 +34,8 @@ interface Props {
onConfirm: () => void;
}
export default function CreateProduct({ onConfirm, onBack }: Props): VNode {
- const { createProduct } = useProductAPI();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -44,7 +45,7 @@ export default function CreateProduct({ onConfirm, onBack }: Props): VNode {
<CreatePage
onBack={onBack}
onCreate={(request: TalerMerchantApi.ProductAddDetail) => {
- return createProduct(request)
+ return lib.instance.addProduct(state.token, request)
.then(() => onConfirm())
.catch((error) => {
setNotif({
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 265146c01..9d5701fa7 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx
@@ -45,6 +45,8 @@ interface Props {
) => Promise<void>;
onCreate: () => void;
selected?: boolean;
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
}
export function CardTable({
@@ -53,6 +55,8 @@ export function CardTable({
onSelect,
onUpdate,
onDelete,
+ onLoadMoreAfter,
+ onLoadMoreBefore
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string | undefined>(
undefined,
@@ -89,6 +93,8 @@ export function CardTable({
onSelect={onSelect}
onDelete={onDelete}
onUpdate={onUpdate}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
rowSelection={rowSelection}
rowSelectionHandler={rowSelectionHandler}
/>
@@ -111,6 +117,8 @@ interface TableProps {
) => Promise<void>;
onDelete: (id: Entity) => void;
rowSelectionHandler: StateUpdater<string | undefined>;
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
}
function Table({
@@ -120,11 +128,18 @@ function Table({
onSelect,
onUpdate,
onDelete,
+ onLoadMoreAfter,
+ onLoadMoreBefore
}: TableProps): VNode {
const { i18n } = useTranslationContext();
const [settings] = usePreference();
return (
<div class="table-container">
+ {onLoadMoreBefore && (
+ <button class="button is-fullwidth" onClick={onLoadMoreBefore}>
+ <i18n.Translate>load first page</i18n.Translate>
+ </button>
+ )}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
<thead>
<tr>
@@ -283,7 +298,7 @@ function Table({
<FastProductUpdateForm
product={i}
onUpdate={(prod) =>
- onUpdate(i.id, prod).then((r) =>
+ onUpdate(i.id, prod).then(() =>
rowSelectionHandler(undefined),
)
}
@@ -297,6 +312,13 @@ function Table({
})}
</tbody>
</table>
+ {onLoadMoreAfter && (
+ <button class="button is-fullwidth"
+ data-tooltip={i18n.str`load more products after the last one`}
+ onClick={onLoadMoreAfter}>
+ <i18n.Translate>load next page</i18n.Translate>
+ </button>
+ )}
</div>
);
}
@@ -341,12 +363,6 @@ function FastProductWithInfiniteStockUpdateForm({
<div class="buttons is-expanded">
- <div class="buttons mt-5">
-
- <button class="button mt-5" onClick={onCancel}>
- <i18n.Translate>Clone</i18n.Translate>
- </button>
- </div>
<div class="buttons is-right mt-5">
<button class="button" onClick={onCancel}>
<i18n.Translate>Cancel</i18n.Translate>
@@ -396,7 +412,7 @@ function FastProductWithManagedStockUpdateForm({
};
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
+ (k) => (errors as Record<string,unknown>)[k] !== undefined,
);
const { i18n } = useTranslationContext();
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 1017a9334..6ad0d4598 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx
@@ -19,60 +19,59 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { ConfirmModal } from "../../../../components/modal/index.js";
+import { useSessionContext } from "../../../../context/session.js";
import {
- useInstanceProducts,
- useProductAPI,
+ useInstanceProducts
} from "../../../../hooks/product.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { CardTable } from "./Table.js";
interface Props {
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
onCreate: () => void;
onSelect: (id: string) => void;
- onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
}
export default function ProductList({
- onUnauthorized,
- onLoadError,
onCreate,
onSelect,
- onNotFound,
}: Props): VNode {
const result = useInstanceProducts();
- const { deleteProduct, updateProduct, getProduct } = useProductAPI();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
const [deleting, setDeleting] =
useState<TalerMerchantApi.ProductDetail & WithId | null>(null);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
return (
@@ -80,31 +79,36 @@ export default function ProductList({
<NotificationCard notification={notif} />
<JumpToElementById
- testIfExist={getProduct}
+ testIfExist={async (id) => {
+ const resp = await lib.instance.getProductDetails(state.token, id);
+ return resp.type === "ok";
+ }}
onSelect={onSelect}
description={i18n.str`jump to product with the given product ID`}
placeholder={i18n.str`product id`}
/>
<CardTable
- instances={result.data}
+ instances={result.body}
+ onLoadMoreBefore={result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
onCreate={onCreate}
- onUpdate={(id, prod) =>
- updateProduct(id, prod)
- .then(() =>
- setNotif({
- message: i18n.str`product updated successfully`,
- type: "SUCCESS",
- }),
- )
- .catch((error) =>
- setNotif({
- message: i18n.str`could not update the product`,
- type: "ERROR",
- description: error.message,
- }),
- )
- }
+ onUpdate={async (id, prod) => {
+ try {
+ await lib.instance.updateProduct(state.token, id, prod);
+ setNotif({
+ message: i18n.str`product updated successfully`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`could not update the product`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ return
+ }}
onSelect={(product) => onSelect(product.id)}
onDelete={(prod: TalerMerchantApi.ProductDetail & WithId) =>
setDeleting(prod)
@@ -120,7 +124,7 @@ export default function ProductList({
onCancel={() => setDeleting(null)}
onConfirm={async (): Promise<void> => {
try {
- await deleteProduct(deleting.id);
+ await lib.instance.deleteProduct(state.token, deleting.id);
setNotif({
message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`,
type: "SUCCESS",
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
index 842462c12..5e3e58d80 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx
@@ -19,66 +19,66 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useProductAPI, useProductDetails } from "../../../../hooks/product.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useProductDetails } from "../../../../hooks/product.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { UpdatePage } from "./UpdatePage.js";
export type Entity = TalerMerchantApi.ProductAddDetail;
interface Props {
onBack?: () => void;
onConfirm: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
pid: string;
}
export default function UpdateProduct({
pid,
onConfirm,
onBack,
- onUnauthorized,
- onNotFound,
- onLoadError,
}: Props): VNode {
- const { updateProduct } = useProductAPI();
const result = useProductDetails(pid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
const { i18n } = useTranslationContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
return (
<Fragment>
<NotificationCard notification={notif} />
<UpdatePage
- product={{ ...result.data, product_id: pid }}
+ product={{ ...result.body, product_id: pid }}
onBack={onBack}
onUpdate={(data) => {
- return updateProduct(pid, data)
+ return lib.instance.updateProduct(state.token, pid, data)
.then(onConfirm)
.catch((error) => {
setNotif({
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
index 31e525226..78d7c83ac 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
@@ -23,13 +23,14 @@ import {
AmountString,
Amounts,
Duration,
+ TalerError,
TalerMerchantApi,
- assertUnreachable,
+ TranslatedString,
} from "@gnu-taler/taler-util";
import {
useTranslationContext
} from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -40,19 +41,13 @@ import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputDuration } from "../../../../components/form/InputDuration.js";
import { InputNumber } from "../../../../components/form/InputNumber.js";
-import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
-import { InputTab } from "../../../../components/form/InputTab.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { TextField } from "../../../../components/form/TextField.js";
import { useSessionContext } from "../../../../context/session.js";
import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
-enum Steps {
- BOTH_FIXED,
- FIXED_PRICE,
- FIXED_SUMMARY,
- NON_FIXED,
-}
-
// type Entity = TalerMerchantApi.TemplateAddDetails & { type: Steps };
type Entity = {
id?: string;
@@ -62,7 +57,9 @@ type Entity = {
amount?: AmountString;
minimum_age?: number;
pay_duration?: Duration;
- type: Steps;
+ summary_editable?: boolean;
+ amount_editable?: boolean;
+ currency_editable?: boolean;
};
interface Props {
@@ -72,9 +69,8 @@ interface Props {
export function CreatePage({ onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const {
- state: { backendUrl },
- } = useSessionContext();
+ const { config } = useSessionContext();
+ const {state:session} = useSessionContext();
const devices = useInstanceOtpDevices();
const [state, setState] = useState<Partial<Entity>>({
@@ -82,9 +78,18 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
pay_duration: {
d_ms: 1000 * 60 * 30, //30 min
},
- type: Steps.NON_FIXED,
});
+ function updateState(up: (s: Partial<Entity>) => Partial<Entity>) {
+ setState((old) => {
+ const newState = up(old);
+ if (!newState.amount_editable) {
+ newState.currency_editable = false;
+ }
+ return newState;
+ });
+ }
+
const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount);
const errors: FormErrors<Entity> = {
@@ -94,24 +99,13 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
? i18n.str`no valid. only characters and numbers`
: undefined,
description: !state.description ? i18n.str`should not be empty` : undefined,
- amount: !(
- state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED
- )
- ? undefined
- : !state.amount
- ? i18n.str`required`
- : !parsedPrice
- ? i18n.str`not valid`
- : Amounts.isZero(parsedPrice)
- ? i18n.str`must be greater than 0`
- : undefined,
- summary: !(
- state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED
- )
+ amount: !state.amount
? undefined
- : !state.summary
- ? i18n.str`required`
- : undefined,
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
minimum_age:
state.minimum_age && state.minimum_age < 0
? i18n.str`should be greater that 0`
@@ -125,68 +119,49 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
: undefined,
};
+ const cList = Object.values(config.currencies).map((d) => d.name);
+
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
);
const submitForm = () => {
- if (hasErrors || state.type === undefined) return Promise.reject();
- switch (state.type) {
- case Steps.FIXED_PRICE:
- return onCreate({
- template_id: state.id!,
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- amount: state.amount!,
- // summary: state.summary,
- },
- otp_id: state.otpId!,
- });
- case Steps.FIXED_SUMMARY:
- return onCreate({
- template_id: state.id!,
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- // amount: state.amount!,
- summary: state.summary,
- },
- otp_id: state.otpId!,
- });
- case Steps.NON_FIXED:
- return onCreate({
- template_id: state.id!,
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- // amount: state.amount!,
- // summary: state.summary,
- },
- otp_id: state.otpId!,
- });
- case Steps.BOTH_FIXED:
- return onCreate({
- template_id: state.id!,
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- amount: state.amount!,
- summary: state.summary,
- },
- otp_id: state.otpId!,
- });
- default:
- assertUnreachable(state.type);
- // return onCreate(state);
- }
+ if (hasErrors) return Promise.reject();
+ return onCreate({
+ template_id: state.id!,
+ template_description: state.description!,
+ template_contract: {
+ minimum_age: state.minimum_age!,
+ pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
+ amount: state.amount_editable ? undefined : state.amount,
+ summary: state.summary_editable ? undefined : state.summary,
+ currency:
+ cList.length > 1 && state.currency_editable
+ ? undefined
+ : config.currency,
+ },
+ editable_defaults: {
+ amount: !state.amount_editable ? undefined : state.amount,
+ summary: !state.summary_editable ? undefined : state.summary,
+ currency:
+ cList.length === 1 || !state.currency_editable
+ ? undefined
+ : config.currency,
+ },
+ otp_id: state.otpId!,
+ });
};
- const deviceList = !devices.ok ? [] : devices.data.otp_devices;
-
+ const deviceList =
+ !devices || devices instanceof TalerError || devices.type === "fail"
+ ? []
+ : devices.body.otp_devices;
+ const deviceMap = deviceList.reduce(
+ (prev, cur) => {
+ prev[cur.otp_device_id] = cur.device_description as TranslatedString;
+ return prev;
+ },
+ {} as Record<string, TranslatedString>,
+ );
return (
<div>
<section class="section is-main-section">
@@ -195,12 +170,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
<div class="column is-four-fifths">
<FormProvider
object={state}
- valueHandler={setState}
+ valueHandler={updateState}
errors={errors}
>
<InputWithAddon<Entity>
name="id"
- help={new URL(`templates/${state.id ?? ""}`, backendUrl).href}
+ help={
+ new URL(`templates/${state.id ?? ""}`, session.backendUrl.href).href
+ }
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`}
/>
@@ -210,59 +187,42 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
help=""
tooltip={i18n.str`Describe what this template stands for`}
/>
- <InputTab<Entity>
- name="type"
- label={i18n.str`Type`}
- help={(() => {
- if (state.type === undefined) return "";
- switch (state.type) {
- case Steps.NON_FIXED:
- return i18n.str`User will be able to input price and summary before payment.`;
- case Steps.FIXED_PRICE:
- return i18n.str`User will be able to add a summary before payment.`;
- case Steps.FIXED_SUMMARY:
- return i18n.str`User will be able to set the price before payment.`;
- case Steps.BOTH_FIXED:
- return i18n.str`User will not be able to change the price or the summary.`;
- }
- })()}
- tooltip={i18n.str`Define what the user be allowed to modify`}
- values={[
- Steps.NON_FIXED,
- Steps.FIXED_PRICE,
- Steps.FIXED_SUMMARY,
- Steps.BOTH_FIXED,
- ]}
- toStr={(v: Steps): string => {
- switch (v) {
- case Steps.NON_FIXED:
- return i18n.str`Simple`;
- case Steps.FIXED_PRICE:
- return i18n.str`With price`;
- case Steps.FIXED_SUMMARY:
- return i18n.str`With summary`;
- case Steps.BOTH_FIXED:
- return i18n.str`With price and summary`;
- }
- }}
+
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
+ />
+ <InputToggle<Entity>
+ name="summary_editable"
+ label={i18n.str`Summary is editable`}
+ tooltip={i18n.str`Allow the user to change the summary.`}
/>
- {state.type === Steps.BOTH_FIXED ||
- state.type === Steps.FIXED_SUMMARY ? (
- <Input<Entity>
- name="summary"
- inputType="multiline"
- label={i18n.str`Fixed summary`}
- tooltip={i18n.str`If specified, this template will create order with the same summary`}
- />
- ) : undefined}
- {state.type === Steps.BOTH_FIXED ||
- state.type === Steps.FIXED_PRICE ? (
- <InputCurrency<Entity>
- name="amount"
- label={i18n.str`Fixed price`}
- tooltip={i18n.str`If specified, this template will create order with the same price`}
- />
- ) : undefined}
+
+ <InputCurrency<Entity>
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`If specified, this template will create order with the same price`}
+ />
+ <InputToggle<Entity>
+ name="amount_editable"
+ label={i18n.str`Amount is editable`}
+ tooltip={i18n.str`Allow the user to select the amount to pay.`}
+ />
+ {cList.length > 1 && (
+ <Fragment>
+ <InputToggle<Entity>
+ name="currency_editable"
+ readonly={!state.amount_editable}
+ label={i18n.str`Currency is editable`}
+ tooltip={i18n.str`Allow the user to change currency.`}
+ />
+ <TextField name="sc" label={i18n.str`Supported currencies`}>
+ <i18n.Translate>supported currencies: {cList.join(", ")}</i18n.Translate>
+ </TextField>
+ </Fragment>
+ )}
<InputNumber<Entity>
name="minimum_age"
label={i18n.str`Minimum age`}
@@ -275,33 +235,34 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
help=""
tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
/>
- <Input<Entity>
- name="otpId"
- label={i18n.str`OTP device`}
- readonly
- side={
- <button
- class="button is-danger"
- data-tooltip={i18n.str`without otp device`}
- onClick={(): void => {
- setState((v) => ({ ...v, otpId: undefined }));
- }}
- >
- <span>
- <i18n.Translate>remove</i18n.Translate>
- </span>
- </button>
- }
- tooltip={i18n.str`Use to verify transaction in offline mode.`}
- />
- <InputSearchOnList
- label={i18n.str`Search device`}
- onChange={(p) => setState((v) => ({ ...v, otpId: p?.id }))}
- list={deviceList.map((e) => ({
- description: e.device_description,
- id: e.otp_device_id,
- }))}
- />
+ {!deviceList.length ? (
+ <TextField
+ name="otpId"
+ label={i18n.str`OTP device`}
+ tooltip={i18n.str`Use to verify transaction while offline.`}
+ >
+ <i18n.Translate>No OTP device.</i18n.Translate>&nbsp;
+ <a href="/otp-devices/new">
+ <i18n.Translate>Add one first</i18n.Translate>
+ </a>
+ </TextField>
+ ) : (
+ <InputSelector<Entity>
+ name="otpId"
+ label={i18n.str`OTP device`}
+ values={[
+ undefined,
+ ...deviceList.map((e) => e.otp_device_id),
+ ]}
+ toStr={(v?: string) => {
+ if (!v) {
+ return i18n.str`No device`;
+ }
+ return deviceMap[v];
+ }}
+ tooltip={i18n.str`Use to verify transaction in offline mode.`}
+ />
+ )}
</FormProvider>
<div class="buttons is-right mt-5">
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx
index 593850268..499c7c859 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx
@@ -21,10 +21,10 @@
import { TalerMerchantApi } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useTemplateAPI } from "../../../../hooks/templates.js";
+import { useSessionContext } from "../../../../context/session.js";
import { Notification } from "../../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
@@ -35,7 +35,8 @@ interface Props {
}
export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
- const { createTemplate } = useTemplateAPI();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -45,7 +46,7 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
<CreatePage
onBack={onBack}
onCreate={(request: TalerMerchantApi.TemplateAddDetails) => {
- return createTemplate(request)
+ return lib.instance.addTemplate(state.token, request)
.then(() => onConfirm())
.catch((error) => {
setNotif({
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
index 84ff9e0f2..66d8a2f7e 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx
@@ -57,9 +57,7 @@ export function ListPage({
onSelect={onSelect}
onNewOrder={onNewOrder}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreBefore={!onLoadMoreBefore}
onLoadMoreAfter={onLoadMoreAfter}
- hasMoreAfter={!onLoadMoreAfter}
/>
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
index 11caca970..082e622e3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx
@@ -34,8 +34,6 @@ interface Props {
onQR: (e: Entity) => void;
onCreate: () => void;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -48,8 +46,6 @@ export function CardTable({
onNewOrder,
onLoadMoreAfter,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
@@ -91,8 +87,6 @@ export function CardTable({
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
/>
) : (
<EmptyTable />
@@ -112,8 +106,6 @@ interface TableProps {
onSelect: (e: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -125,19 +117,17 @@ function Table({
onQR,
onSelect,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
return (
<div class="table-container">
- {hasMoreBefore && (
+ {onLoadMoreBefore && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more templates before the first one`}
onClick={onLoadMoreBefore}
>
- <i18n.Translate>load newer templates</i18n.Translate>
+ <i18n.Translate>load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -198,13 +188,13 @@ function Table({
})}
</tbody>
</table>
- {hasMoreAfter && (
+ {onLoadMoreAfter && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more templates after the last one`}
onClick={onLoadMoreAfter}
>
- <i18n.Translate>load older templates</i18n.Translate>
+ <i18n.Translate>load next page</i18n.Translate>
</button>
)}
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
index 40ca6ac98..9e59609c7 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx
@@ -19,29 +19,27 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { ConfirmModal } from "../../../../components/modal/index.js";
+import { useSessionContext } from "../../../../context/session.js";
import {
- useInstanceTemplates,
- useTemplateAPI,
+ useInstanceTemplates
} from "../../../../hooks/templates.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { ListPage } from "./ListPage.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
- onNotFound: () => VNode;
onCreate: () => void;
onSelect: (id: string) => void;
onNewOrder: (id: string) => void;
@@ -49,35 +47,35 @@ interface Props {
}
export default function ListTemplates({
- onUnauthorized,
- onLoadError,
onCreate,
onQR,
onSelect,
onNewOrder,
- onNotFound,
}: Props): VNode {
- const [position, setPosition] = useState<string | undefined>(undefined);
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { deleteTemplate, testTemplateExist } = useTemplateAPI();
- const result = useInstanceTemplates({ position }, (id) => setPosition(id));
+ const { lib } = useSessionContext();
+ const result = useInstanceTemplates();
const [deleting, setDeleting] =
useState<TalerMerchantApi.TemplateEntry | null>(null);
+ const { state } = useSessionContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
}
return (
@@ -85,18 +83,21 @@ export default function ListTemplates({
<NotificationCard notification={notif} />
<JumpToElementById
- testIfExist={testTemplateExist}
+ testIfExist={async (id) => {
+ const resp = await lib.instance.getTemplateDetails(state.token, id)
+ return resp.type === "ok"
+ }}
onSelect={onSelect}
description={i18n.str`jump to template with the given template ID`}
placeholder={i18n.str`template id`}
/>
<ListPage
- templates={result.data.templates}
+ templates={result.body}
onLoadMoreBefore={
- result.isReachingStart ? result.loadMorePrev : undefined
+ result.isFirstPage ? undefined: result.loadFirst
}
- onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
onCreate={onCreate}
onSelect={(e) => {
onSelect(e.template_id);
@@ -122,7 +123,7 @@ export default function ListTemplates({
onCancel={() => setDeleting(null)}
onConfirm={async (): Promise<void> => {
try {
- await deleteTemplate(deleting.template_id);
+ await lib.instance.deleteTemplate(state.token, deleting.template_id);
setNotif({
message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`,
type: "SUCCESS",
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
index d48e5e956..7322ca169 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
@@ -19,23 +19,18 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerMerchantApi, stringifyPayTemplateUri } from "@gnu-taler/taler-util";
import {
- useMerchantApiContext,
- useTranslationContext,
+ TalerMerchantApi,
+ stringifyPayTemplateUri
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
-import { useState } from "preact/hooks";
import { QR } from "../../../../components/exception/QR.js";
-import {
- FormErrors,
- FormProvider,
-} from "../../../../components/form/FormProvider.js";
-import { Input } from "../../../../components/form/Input.js";
-import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { useSessionContext } from "../../../../context/session.js";
-type Entity = TalerMerchantApi.UsingTemplateDetails;
+// type Entity = TalerMerchantApi.UsingTemplateDetails;
interface Props {
contract: TalerMerchantApi.TemplateContractDetails;
@@ -43,86 +38,82 @@ interface Props {
onBack?: () => void;
}
-export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
+export function QrPage({ id: templateId, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const {
- state: { backendUrl },
- } = useSessionContext();
- const { config } = useMerchantApiContext();
-
- const [state, setState] = useState<Partial<Entity>>({
- amount: contract.amount,
- summary: contract.summary,
- });
+ const { state } = useSessionContext();
- const errors: FormErrors<Entity> = {};
+ // const [state, setState] = useState<Partial<Entity>>({
+ // amount: contract.amount,
+ // summary: contract.summary,
+ // });
- const fixedAmount = !!contract.amount;
- const fixedSummary = !!contract.summary;
+ // const errors: FormErrors<Entity> = {};
- const templateParams: Record<string, string> = {};
- if (!fixedAmount) {
- if (state.amount) {
- templateParams.amount = state.amount;
- } else {
- templateParams.amount = config.currency;
- }
- }
+ // const fixedAmount = !!contract.amount;
+ // const fixedSummary = !!contract.summary;
- if (!fixedSummary) {
- templateParams.summary = state.summary ?? "";
- }
+ // const templateParams: Record<string, string> = {};
+ // if (!fixedAmount) {
+ // if (state.amount) {
+ // templateParams.amount = state.amount;
+ // } else {
+ // templateParams.amount = config.currency;
+ // }
+ // }
- const merchantBaseUrl = backendUrl;
+ // if (!fixedSummary) {
+ // templateParams.summary = state.summary ?? "";
+ // }
+
+ const merchantBaseUrl = state.backendUrl.href;
const payTemplateUri = stringifyPayTemplateUri({
merchantBaseUrl,
templateId,
- templateParams,
+ templateParams: {},
});
return (
<div>
+ <section id="printThis">
+ <QR text={payTemplateUri} />
+ <pre style={{ textAlign: "center" }}>
+ <a href={payTemplateUri}>{payTemplateUri}</a>
+ </pre>
+ </section>
+
<section class="section is-main-section">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
- <p class="is-size-5 mt-5 mb-5">
+ {/* <p class="is-size-5 mt-5 mb-5">
<i18n.Translate>
Here you can specify a default value for fields that are not
fixed. Default values can be edited by the customer before the
payment.
</i18n.Translate>
- </p>
+ </p> */}
<p></p>
- <FormProvider
+ {/* <FormProvider
object={state}
valueHandler={setState}
errors={errors}
>
<InputCurrency<Entity>
name="amount"
- label={
- fixedAmount
- ? i18n.str`Fixed amount`
- : i18n.str`Default amount`
- }
- readonly={fixedAmount}
+ label={i18n.str`Amount`}
+ readonly
tooltip={i18n.str`Amount of the order`}
/>
<Input<Entity>
name="summary"
inputType="multiline"
- readonly={fixedSummary}
- label={
- fixedSummary
- ? i18n.str`Fixed summary`
- : i18n.str`Default summary`
- }
+ readonly
+ label={i18n.str`Summary`}
tooltip={i18n.str`Title of the order to be shown to the customer`}
/>
- </FormProvider>
+ </FormProvider> */}
<div class="buttons is-right mt-5">
{onBack && (
@@ -141,12 +132,6 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
<div class="column" />
</div>
</section>
- <section id="printThis">
- <QR text={payTemplateUri} />
- <pre style={{ textAlign: "center" }}>
- <a href={payTemplateUri}>{payTemplateUri}</a>
- </pre>
- </section>
</div>
);
}
@@ -164,6 +149,6 @@ function saveAsPDF(name: string): void {
printWindow.document.body.appendChild(divContents.cloneNode(true));
printWindow.addEventListener("load", () => {
printWindow.print();
- printWindow.close();
+ // printWindow.close();
});
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
index 37f0e5c74..ed809c7b3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx
@@ -19,59 +19,48 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
-import {
- ErrorType,
- HttpError
-} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
-import { NotificationCard } from "../../../../components/menu/index.js";
import {
useTemplateDetails
} from "../../../../hooks/templates.js";
-import { Notification } from "../../../../utils/types.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { QrPage } from "./QrPage.js";
+import { LoginPage } from "../../../login/index.js";
export type Entity = TalerMerchantApi.TransferInformation;
interface Props {
onBack?: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
tid: string;
}
export default function TemplateQrPage({
tid,
onBack,
- onLoadError,
- onNotFound,
- onUnauthorized,
}: Props): VNode {
const result = useTemplateDetails(tid);
- const [notif, setNotif] = useState<Notification | undefined>(undefined);
-
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
}
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
+ }
+
return (
- <>
- <NotificationCard notification={notif} />
- <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} />
- </>
+ <QrPage contract={result.body.template_contract} id={tid} onBack={onBack} />
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
index f4092b61b..eedb77f28 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -23,11 +23,14 @@ import {
AmountString,
Amounts,
Duration,
+ TalerError,
TalerMerchantApi,
- assertUnreachable
+ TranslatedString,
} from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -38,83 +41,88 @@ import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputDuration } from "../../../../components/form/InputDuration.js";
import { InputNumber } from "../../../../components/form/InputNumber.js";
-import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
-import { InputTab } from "../../../../components/form/InputTab.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
+import { TextField } from "../../../../components/form/TextField.js";
import { useSessionContext } from "../../../../context/session.js";
import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
-enum Steps {
- BOTH_FIXED,
- FIXED_PRICE,
- FIXED_SUMMARY,
- NON_FIXED,
-}
-
type Entity = {
- description?: string,
- otpId?: string | null,
- summary?: string,
- amount?: AmountString,
- minimum_age?: number,
- pay_duration?: Duration,
+ description?: string;
+ otpId?: string | null;
+ summary?: string;
+ amount?: AmountString;
+ minimum_age?: number;
+ pay_duration?: Duration;
+ summary_editable?: boolean;
+ amount_editable?: boolean;
+ currency_editable?: boolean;
};
interface Props {
onUpdate: (d: TalerMerchantApi.TemplatePatchDetails) => Promise<void>;
onBack?: () => void;
- template: TalerMerchantApi.TemplateDetails;
+ template: TalerMerchantApi.TemplateDetails & WithId;
}
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const {
- state: { backendUrl },
- } = useSessionContext();
-
+ const { config } = useSessionContext();
+ const {state:session} = useSessionContext();
- const intialStep =
- template.template_contract.amount === undefined && template.template_contract.summary === undefined
- ? Steps.NON_FIXED
- : template.template_contract.summary === undefined
- ? Steps.FIXED_PRICE
- : template.template_contract.amount === undefined
- ? Steps.FIXED_SUMMARY
- : Steps.BOTH_FIXED;
-
- const [state, setState] = useState<Partial<Entity & { type: Steps }>>({
- amount: template.template_contract.amount as AmountString | undefined,
+ const [state, setState] = useState<Partial<Entity>>({
description: template.template_description,
minimum_age: template.template_contract.minimum_age,
otpId: template.otp_id,
- pay_duration: template.template_contract.pay_duration ? Duration.fromTalerProtocolDuration(template.template_contract.pay_duration) : undefined,
- summary: template.template_contract.summary,
- type: intialStep,
+ pay_duration: template.template_contract.pay_duration
+ ? Duration.fromTalerProtocolDuration(
+ template.template_contract.pay_duration,
+ )
+ : undefined,
+ summary:
+ template.editable_defaults?.summary ?? template.template_contract.summary,
+ amount:
+ template.editable_defaults?.amount ??
+ (template.template_contract.amount as AmountString | undefined),
+ currency_editable: !!template.editable_defaults?.currency,
+ summary_editable: !!template.editable_defaults?.summary,
+ amount_editable: !!template.editable_defaults?.amount,
});
- const devices = useInstanceOtpDevices()
- const deviceList = !devices.ok ? [] : devices.data.otp_devices
- const parsedPrice = !state.amount
- ? undefined
- : Amounts.parse(state.amount);
+ function updateState(up: (s: Partial<Entity>) => Partial<Entity>) {
+ setState((old) => {
+ const newState = up(old);
+ if (!newState.amount_editable) {
+ newState.currency_editable = false;
+ }
+ return newState;
+ });
+ }
+
+ const devices = useInstanceOtpDevices();
+ const deviceList =
+ !devices || devices instanceof TalerError || devices.type === "fail"
+ ? []
+ : devices.body.otp_devices;
+ const deviceMap = deviceList.reduce(
+ (prev, cur) => {
+ prev[cur.otp_device_id] = cur.device_description as TranslatedString;
+ return prev;
+ },
+ {} as Record<string, TranslatedString>,
+ );
+
+ const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount);
const errors: FormErrors<Entity> = {
- description: !state.description
- ? i18n.str`should not be empty`
- : undefined,
- amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED)
+ description: !state.description ? i18n.str`should not be empty` : undefined,
+ amount: !state.amount
? undefined
- : !state.amount
- ? i18n.str`required`
- : !parsedPrice
- ? i18n.str`not valid`
- : Amounts.isZero(parsedPrice)
- ? i18n.str`must be greater than 0`
- : undefined,
- summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED)
- ? undefined
- : !state.summary
- ? i18n.str`required`
- : undefined,
+ : !parsedPrice
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
minimum_age:
state.minimum_age && state.minimum_age < 0
? i18n.str`should be greater that 0`
@@ -128,58 +136,38 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
: undefined,
};
+ const cList = Object.values(config.currencies).map((d) => d.name);
+
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
);
const submitForm = () => {
- if (hasErrors || state.type === undefined) return Promise.reject();
- switch (state.type) {
- case Steps.FIXED_PRICE: return onUpdate({
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- amount: state.amount!,
- // summary: state.summary,
- },
- otp_id: state.otpId!
- })
- case Steps.FIXED_SUMMARY: return onUpdate({
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- // amount: state.amount!,
- summary: state.summary,
- },
- otp_id: state.otpId!,
- })
- case Steps.NON_FIXED: return onUpdate({
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- // amount: state.amount!,
- // summary: state.summary,
- },
- otp_id: state.otpId!,
- })
- case Steps.BOTH_FIXED: return onUpdate({
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- amount: state.amount!,
- summary: state.summary,
- },
- otp_id: state.otpId!,
- })
- default: assertUnreachable(state.type)
- }
+ if (hasErrors) return Promise.reject();
+ return onUpdate({
+ template_description: state.description!,
+ template_contract: {
+ minimum_age: state.minimum_age!,
+ pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
+ amount: state.amount_editable ? undefined : state.amount,
+ summary: state.summary_editable ? undefined : state.summary,
+ currency:
+ cList.length > 1 && state.currency_editable
+ ? undefined
+ : config.currency,
+ },
+ editable_defaults: {
+ amount: !state.amount_editable ? undefined : state.amount,
+ summary: !state.summary_editable ? undefined : state.summary,
+ currency:
+ cList.length === 1 || !state.currency_editable
+ ? undefined
+ : config.currency,
+ },
+ otp_id: state.otpId!,
+ });
};
-
return (
<div>
<section class="section">
@@ -189,7 +177,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
- {new URL(`templates/${template.otp_id}`,backendUrl).href}
+ {new URL(`templates/${template.id}`, session.backendUrl.href).href}
</span>
</div>
</div>
@@ -203,58 +191,51 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
<div class="column is-four-fifths">
<FormProvider
object={state}
- valueHandler={setState}
+ valueHandler={updateState}
errors={errors}
>
-
<Input<Entity>
name="description"
label={i18n.str`Description`}
help=""
tooltip={i18n.str`Describe what this template stands for`}
/>
- <InputTab
- name="type"
- label={i18n.str`Type`}
- help={(() => {
- switch (state.type) {
- case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.`
- case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.`
- case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.`
- case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.`
- }
- })()}
- tooltip={i18n.str`Define what the user be allowed to modify`}
- values={[
- Steps.NON_FIXED,
- Steps.FIXED_PRICE,
- Steps.FIXED_SUMMARY,
- Steps.BOTH_FIXED,
- ]}
- toStr={(v: Steps): string => {
- switch (v) {
- case Steps.NON_FIXED: return i18n.str`Simple`
- case Steps.FIXED_PRICE: return i18n.str`With price`
- case Steps.FIXED_SUMMARY: return i18n.str`With summary`
- case Steps.BOTH_FIXED: return i18n.str`With price and summary`
- }
- }}
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
/>
- {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ?
- <Input<Entity>
- name="summary"
- inputType="multiline"
- label={i18n.str`Fixed summary`}
- tooltip={i18n.str`If specified, this template will create order with the same summary`}
- />
- : undefined}
- {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ?
- <InputCurrency<Entity>
- name="amount"
- label={i18n.str`Fixed price`}
- tooltip={i18n.str`If specified, this template will create order with the same price`}
- />
- : undefined}
+ <InputToggle<Entity>
+ name="summary_editable"
+ label={i18n.str`Summary is editable`}
+ tooltip={i18n.str`Allow the user to change the summary.`}
+ />
+ <InputCurrency<Entity>
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`If specified, this template will create order with the same price`}
+ />
+ <InputToggle<Entity>
+ name="amount_editable"
+ label={i18n.str`Amount is editable`}
+ tooltip={i18n.str`Allow the user to select the amount to pay.`}
+ />
+ {cList.length > 1 && (
+ <Fragment>
+ <InputToggle<Entity>
+ name="currency_editable"
+ readonly={!state.amount_editable}
+ label={i18n.str`Currency is editable`}
+ tooltip={i18n.str`Allow the user to change currency.`}
+ />
+ <TextField name="sc" label={i18n.str`Supported currencies`}>
+ <i18n.Translate>
+ supported currencies: {cList.join(", ")}
+ </i18n.Translate>
+ </TextField>
+ </Fragment>
+ )}
<InputNumber<Entity>
name="minimum_age"
label={i18n.str`Minimum age`}
@@ -267,31 +248,34 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
help=""
tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
/>
- <Input<Entity>
- name="otpId"
- label={i18n.str`OTP device`}
- readonly
- side={<button
- class="button is-danger"
- data-tooltip={i18n.str`remove otp device for this template`}
- onClick={(): void => {
- setState((v) => ({ ...v, otpId: null }));
- }}
+ {!deviceList.length ? (
+ <TextField
+ name="otpId"
+ label={i18n.str`OTP device`}
+ tooltip={i18n.str`Use to verify transaction while offline.`}
>
- <span>
- <i18n.Translate>remove</i18n.Translate>
- </span>
- </button>}
- tooltip={i18n.str`Use to verify transaction in offline mode.`}
- />
- <InputSearchOnList
- label={i18n.str`Search device`}
- onChange={(p) => setState((v) => ({ ...v, otpId: p?.id }))}
- list={deviceList.map(e => ({
- description: e.device_description,
- id: e.otp_device_id
- }))}
- />
+ <i18n.Translate>No OTP device.</i18n.Translate>&nbsp;
+ <a href="/otp-devices/new">
+ <i18n.Translate>Add one first</i18n.Translate>
+ </a>
+ </TextField>
+ ) : (
+ <InputSelector<Entity>
+ name="otpId"
+ label={i18n.str`OTP device`}
+ values={[
+ undefined,
+ ...deviceList.map((e) => e.otp_device_id),
+ ]}
+ toStr={(v?: string) => {
+ if (!v) {
+ return i18n.str`No device`;
+ }
+ return deviceMap[v];
+ }}
+ tooltip={i18n.str`Use to verify transaction in offline mode.`}
+ />
+ )}
</FormProvider>
<div class="buttons is-right mt-5">
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
index ba1939914..6185bd2a9 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx
@@ -19,21 +19,22 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
import {
- useTemplateAPI,
useTemplateDetails,
} from "../../../../hooks/templates.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { UpdatePage } from "./UpdatePage.js";
export type Entity = TalerMerchantApi.TemplatePatchDetails & WithId;
@@ -41,48 +42,46 @@ export type Entity = TalerMerchantApi.TemplatePatchDetails & WithId;
interface Props {
onBack?: () => void;
onConfirm: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
tid: string;
}
export default function UpdateTemplate({
tid,
onConfirm,
onBack,
- onUnauthorized,
- onNotFound,
- onLoadError,
}: Props): VNode {
- const { updateTemplate } = useTemplateAPI();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
const result = useTemplateDetails(tid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
}
return (
<Fragment>
<NotificationCard notification={notif} />
<UpdatePage
- template={{ ...result.data }}
+ template={{...result.body, id: tid}}
onBack={onBack}
onUpdate={(data) => {
- return updateTemplate(tid, data)
+ return lib.instance.updateTemplate(state.token, tid, data)
.then(onConfirm)
.catch((error) => {
setNotif({
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
index 58e63cc8e..360c9d373 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
@@ -31,7 +31,7 @@ import {
import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
-type Entity = TalerMerchantApi.UsingTemplateDetails;
+type Entity = TalerMerchantApi.TemplateContractDetails;
interface Props {
id: string;
@@ -44,17 +44,18 @@ export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const [state, setState] = useState<Partial<Entity>>({
- amount: template.template_contract.amount,
- summary: template.template_contract.summary,
+ currency: template.editable_defaults?.currency ?? template.template_contract.currency,
+ amount: template.editable_defaults?.amount ?? template.template_contract.amount,
+ summary: template.editable_defaults?.summary ?? template.template_contract.summary,
});
const errors: FormErrors<Entity> = {
amount:
- !template.template_contract.amount && !state.amount
+ !state.amount
? i18n.str`Amount is required`
: undefined,
summary:
- !template.template_contract.summary && !state.summary
+ !state.summary
? i18n.str`Order summary is required`
: undefined,
};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
index 64c38c86b..00cb2b827 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx
@@ -19,30 +19,28 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import {
- useTemplateAPI,
- useTemplateDetails,
+ useTemplateDetails
} from "../../../../hooks/templates.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { UsePage } from "./UsePage.js";
+import { useSessionContext } from "../../../../context/session.js";
export type Entity = TalerMerchantApi.TransferInformation;
interface Props {
onBack?: () => void;
onOrderCreated: (id: string) => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
tid: string;
}
@@ -50,42 +48,52 @@ export default function TemplateUsePage({
tid,
onOrderCreated,
onBack,
- onLoadError,
- onNotFound,
- onUnauthorized,
}: Props): VNode {
- const { createOrderFromTemplate } = useTemplateAPI();
+ const { lib } = useSessionContext();
const result = useTemplateDetails(tid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
}
return (
<>
<NotificationCard notification={notif} />
<UsePage
- template={result.data}
+ template={result.body}
id={tid}
onBack={onBack}
onCreateOrder={(
request: TalerMerchantApi.UsingTemplateDetails,
) => {
- return createOrderFromTemplate(tid, request)
- .then((res) => onOrderCreated(res.data.order_id))
+
+ return lib.instance.useTemplateCreateOrder(tid, request)
+ .then((res) => {
+ if (res.type === "ok") {
+ onOrderCreated(res.body.order_id)
+ } else {
+ setNotif({
+ message: i18n.str`could not create order from template`,
+ type: "ERROR",
+ });
+ }
+ })
.catch((error) => {
setNotif({
message: i18n.str`could not create order from template`,
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 c833b908c..d718ffb69 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
@@ -27,7 +27,7 @@ import { FormProvider } from "../../../components/form/FormProvider.js";
import { Input } from "../../../components/form/Input.js";
import { NotificationCard } from "../../../components/menu/index.js";
import { useSessionContext } from "../../../context/session.js";
-import { AccessToken } from "@gnu-taler/taler-util";
+import { AccessToken, createRFC8959AccessTokenPlain } from "@gnu-taler/taler-util";
interface Props {
hasToken: boolean | undefined;
@@ -67,7 +67,7 @@ export function DetailPage({
};
const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
+ (k) => (errors as Record<string, unknown>)[k] !== undefined,
);
const { state } = useSessionContext();
@@ -76,10 +76,11 @@ export function DetailPage({
async function submitForm() {
if (hasErrors) return;
- const oldToken = hasToken
- ? (form.old_token as AccessToken)
- : undefined;
- const newToken = form.new_token as AccessToken;
+ const oldToken =
+ form.old_token !== undefined && hasToken
+ ? createRFC8959AccessTokenPlain(form.old_token)
+ : undefined;
+ const newToken = createRFC8959AccessTokenPlain(form.new_token!);
onNewToken(oldToken, newToken);
}
@@ -133,8 +134,7 @@ export function DetailPage({
class="button"
onClick={() => {
if (hasToken) {
- const oldToken = form.old_token as AccessToken;
- onClearToken(oldToken);
+ onClearToken(form.old_token ? createRFC8959AccessTokenPlain(form.old_token) : undefined);
} else {
onClearToken(undefined);
}
@@ -159,25 +159,26 @@ export function DetailPage({
inputType="password"
/>
</Fragment>
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <a class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ )}
+ <AsyncButton
+ type="submit"
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm change</i18n.Translate>
+ </AsyncButton>
+ </div>
</FormProvider>
- <div class="buttons is-right mt-5">
- {onBack && (
- <button class="button" onClick={onBack}>
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- )}
- <AsyncButton
- disabled={hasErrors}
- data-tooltip={
- hasErrors
- ? i18n.str`Need to complete marked fields`
- : "confirm operation"
- }
- onClick={submitForm}
- >
- <i18n.Translate>Confirm change</i18n.Translate>
- </AsyncButton>
- </div>
</div>
<div class="column" />
</div>
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 f3c9a52ea..c23e5be17 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
@@ -13,55 +13,57 @@
You should have received a 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, TalerErrorDetail } from "@gnu-taler/taler-util";
-import { ErrorType, HttpError, useMerchantApiContext, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../components/exception/loading.js";
import { NotificationCard } from "../../../components/menu/index.js";
import { useSessionContext } from "../../../context/session.js";
import { useInstanceDetails } from "../../../hooks/instance.js";
import { Notification } from "../../../utils/types.js";
+import { LoginPage } from "../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../notfound/index.js";
import { DetailPage } from "./DetailPage.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
onChange: () => void;
- onNotFound: () => VNode;
onCancel: () => void;
}
-export default function Token({
- onLoadError,
- onChange,
- onUnauthorized,
- onNotFound,
- onCancel,
-}: Props): VNode {
+export default function Token({ onChange, onCancel }: Props): VNode {
const { i18n } = useTranslationContext();
- const { lib } = useMerchantApiContext();
+ const { lib } = useSessionContext();
const { logIn } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- // const { clearAccessToken } = useInstanceAPI();
- const result = useInstanceDetails()
+ 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);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />;
+ }
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
- const hasToken = result.data.auth.type === "token"
+ const hasToken = result.body.auth.method === "token";
return (
<Fragment>
@@ -71,13 +73,24 @@ export default function Token({
hasToken={hasToken}
onClearToken={async (currentToken): Promise<void> => {
try {
- await lib.management.updateCurrentInstanceAuthentication(currentToken, {
- method: "external",
- })
- onChange();
+ const resp = await lib.instance.updateCurrentInstanceAuthentication(
+ currentToken,
+ {
+ method: "external",
+ },
+ );
+ if (resp.type === "ok") {
+ onChange();
+ } else {
+ return setNotif({
+ message: i18n.str`Failed to clear token`,
+ type: "ERROR",
+ description: resp.detail.hint,
+ });
+ }
} catch (error) {
if (error instanceof Error) {
- setNotif({
+ return setNotif({
message: i18n.str`Failed to clear token`,
type: "ERROR",
description: error.message,
@@ -87,29 +100,45 @@ export default function Token({
}}
onNewToken={async (currentToken, newToken): Promise<void> => {
try {
- await lib.management.updateCurrentInstanceAuthentication(currentToken, {
- token: newToken,
- method: "token"
- })
- const resp = await lib.authenticate.createAccessTokenBearer(newToken, {
- scope: "write",
- duration: {
- d_us: "forever"
+ {
+ const resp =
+ await lib.instance.updateCurrentInstanceAuthentication(
+ currentToken,
+ {
+ token: newToken,
+ method: "token",
+ },
+ );
+ if (resp.type === "fail") {
+ return setNotif({
+ message: i18n.str`Failed to set new token`,
+ type: "ERROR",
+ description: resp.detail.hint,
+ });
+ }
+ }
+ const resp = await lib.authenticate.createAccessTokenBearer(
+ newToken,
+ {
+ scope: "write",
+ duration: {
+ d_us: "forever",
+ },
+ refreshable: true,
},
- refreshable: true,
- })
+ );
if (resp.type === "ok") {
- logIn({ token: resp.body.token })
- onChange();
+ logIn(resp.body.token);
+ return onChange();
} else {
- setNotif({
+ return setNotif({
message: i18n.str`Failed to set new token`,
type: "ERROR",
});
}
} catch (error) {
if (error instanceof Error) {
- setNotif({
+ return setNotif({
message: i18n.str`Failed to set new token`,
type: "ERROR",
description: error.message,
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx
index e640e47f6..428476337 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx
@@ -19,13 +19,15 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerMerchantApi } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { TalerError, TalerMerchantApi } from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
-import { useTransferAPI } from "../../../../hooks/transfer.js";
import { Notification } from "../../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
@@ -36,13 +38,15 @@ interface Props {
}
export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
- const { informTransfer } = useTransferAPI();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
const instance = useInstanceBankAccounts();
- const accounts = !instance.ok
- ? []
- : instance.data.accounts.map((a) => a.payto_uri);
+ const accounts =
+ !instance || instance instanceof TalerError || instance.type === "fail"
+ ? []
+ : instance.body.accounts.map((a) => a.payto_uri);
return (
<>
@@ -51,7 +55,8 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
onBack={onBack}
accounts={accounts}
onCreate={(request: TalerMerchantApi.TransferInformation) => {
- return informTransfer(request)
+ return lib.instance
+ .informWireTransfer(state.token, request)
.then(() => onConfirm())
.catch((error) => {
setNotif({
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
index 7b54dc5ed..22ad0b8d8 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
@@ -75,6 +75,11 @@ export function ListPage({
name="payto_uri"
label={i18n.str`Account URI`}
values={accounts}
+ fromStr={(d) => {
+ const idx = accounts.indexOf(d)
+ if (idx === -1) return undefined;
+ return d
+ }}
placeholder={i18n.str`Select one account`}
tooltip={i18n.str`filter by account address`}
/>
@@ -125,9 +130,7 @@ export function ListPage({
onCreate={onCreate}
onDelete={onDelete}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreBefore={!onLoadMoreBefore}
onLoadMoreAfter={onLoadMoreAfter}
- hasMoreAfter={!onLoadMoreAfter}
/>
</section>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
index cf7ebe922..b9235c669 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
@@ -34,8 +34,6 @@ interface Props {
onCreate: () => void;
accounts: string[];
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -45,8 +43,6 @@ export function CardTable({
onDelete,
onLoadMoreAfter,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
@@ -85,8 +81,6 @@ export function CardTable({
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
/>
) : (
<EmptyTable />
@@ -103,8 +97,6 @@ interface TableProps {
onDelete: (id: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -113,20 +105,18 @@ function Table({
onLoadMoreAfter,
onDelete,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
const [settings] = usePreference();
return (
<div class="table-container">
- {hasMoreBefore && (
+ {onLoadMoreBefore && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more transfers before the first one`}
onClick={onLoadMoreBefore}
>
- <i18n.Translate>load newer transfers</i18n.Translate>
+ <i18n.Translate>load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -192,13 +182,13 @@ function Table({
})}
</tbody>
</table>
- {hasMoreAfter && (
+ {onLoadMoreAfter && (
<button
class="button is-fullwidth"
- data-tooltip={i18n.str`load more transfer after the last one`}
+ data-tooltip={i18n.str`load more transfers after the last one`}
onClick={onLoadMoreAfter}
>
- <i18n.Translate>load older transfers</i18n.Translate>
+ <i18n.Translate>load next page</i18n.Translate>
</button>
)}
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
index 15706b4c5..8b4d1f3cb 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
@@ -19,40 +19,36 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode, TalerErrorDetail } from "@gnu-taler/taler-util";
-import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
+import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
import { useInstanceTransfers } from "../../../../hooks/transfer.js";
+import { LoginPage } from "../../../login/index.js";
import { ListPage } from "./ListPage.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
- onNotFound: () => VNode;
onCreate: () => void;
}
interface Form {
- verified?: "yes" | "no";
+ verified?: boolean;
payto_uri?: string;
}
export default function ListTransfer({
- onUnauthorized,
- onLoadError,
onCreate,
- onNotFound,
}: Props): VNode {
- const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s });
+ const setFilter = (s?: boolean) => setForm({ ...form, verified: s });
const [position, setPosition] = useState<string | undefined>(undefined);
const instance = useInstanceBankAccounts();
- const accounts = !instance.ok
+ const accounts = !instance || (instance instanceof TalerError) || instance.type === "fail"
? []
- : instance.data.accounts.map((a) => a.payto_uri);
+ : instance.body.accounts.map((a) => a.payto_uri);
const [form, setForm] = useState<Form>({ payto_uri: "" });
const shoulUseDefaultAccount = accounts.length === 1
@@ -62,8 +58,8 @@ export default function ListTransfer({
}
}, [shoulUseDefaultAccount])
- const isVerifiedTransfers = form.verified === "yes";
- const isNonVerifiedTransfers = form.verified === "no";
+ const isVerifiedTransfers = form.verified === true;
+ const isNonVerifiedTransfers = form.verified === false;
const isAllTransfers = form.verified === undefined;
const result = useInstanceTransfers(
@@ -75,37 +71,37 @@ export default function ListTransfer({
(id) => setPosition(id),
);
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
return (
<ListPage
accounts={accounts}
- transfers={result.data.transfers}
- onLoadMoreBefore={
- result.isReachingStart ? result.loadMorePrev : undefined
- }
- onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ transfers={result.body}
+ onLoadMoreBefore={result.isFirstPage ? undefined: result.loadFirst }
+ onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext}
onCreate={onCreate}
onDelete={() => {
null;
}}
- // position={position} setPosition={setPosition}
onShowAll={() => setFilter(undefined)}
- onShowUnverified={() => setFilter("no")}
- onShowVerified={() => setFilter("yes")}
+ onShowUnverified={() => setFilter(false)}
+ onShowVerified={() => setFilter(true)}
isAllTransfers={isAllTransfers}
isVerifiedTransfers={isVerifiedTransfers}
isNonVerifiedTransfers={isNonVerifiedTransfers}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx
index 5514b68d9..5bd12e4e9 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx
@@ -43,7 +43,7 @@ function createExample<Props>(
export const Example = createExample(TestedComponent, {
selected: {
name: "name",
- auth: { type: "external" },
+ auth: { method: "external" },
address: {},
user_type: "business",
use_stefan: true,
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 32e4e149c..9da7f7efb 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
@@ -13,47 +13,46 @@
You should have received a 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, TalerErrorDetail, TalerMerchantApi, TalerMerchantInstanceHttpClient } from "@gnu-taler/taler-util";
+import { HttpStatusCode, TalerError, TalerMerchantApi, TalerMerchantInstanceHttpClient, TalerMerchantManagementResultByMethod, assertUnreachable } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- HttpResponse,
- useMerchantApiContext,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../components/exception/loading.js";
import { NotificationCard } from "../../../components/menu/index.js";
+import { useSessionContext } from "../../../context/session.js";
import {
useInstanceDetails,
useManagedInstanceDetails,
} from "../../../hooks/instance.js";
import { Notification } from "../../../utils/types.js";
+import { LoginPage } from "../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../notfound/index.js";
import { UpdatePage } from "./UpdatePage.js";
-import { useSessionContext } from "../../../context/session.js";
export interface Props {
onBack: () => void;
onConfirm: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
- onUpdateError: (e: HttpError<TalerErrorDetail>) => void;
+ // onUnauthorized: () => VNode;
+ // onNotFound: () => VNode;
+ // onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
+ // onUpdateError: (e: HttpError<TalerErrorDetail>) => void;
}
export default function Update(props: Props): VNode {
- const { lib } = useMerchantApiContext();
- const updateInstance = lib.management.updateCurrentInstance.bind(lib.management)
+ const { lib } = useSessionContext();
+ const updateInstance = lib.instance.updateCurrentInstance.bind(lib.instance)
const result = useInstanceDetails();
return CommonUpdate(props, result, updateInstance,);
}
export function AdminUpdate(props: Props & { instanceId: string }): VNode {
- const { lib } = useMerchantApiContext();
- const t = lib.instance(props.instanceId)
- const updateInstance = lib.instance(props.instanceId).updateCurrentInstance.bind(t)
+ const { lib } = useSessionContext();
+ const t = lib.subInstanceApi(props.instanceId).instance;
+ const updateInstance = t.updateCurrentInstance.bind(t)
const result = useManagedInstanceDetails(props.instanceId);
return CommonUpdate(props, result, updateInstance,);
}
@@ -63,33 +62,30 @@ function CommonUpdate(
{
onBack,
onConfirm,
- onLoadError,
- onNotFound,
- onUnauthorized,
}: Props,
- result: HttpResponse<
- TalerMerchantApi.QueryInstancesResponse,
- TalerErrorDetail
- >,
+ result: TalerMerchantManagementResultByMethod<"getInstanceDetails"> | TalerError | undefined,
updateInstance: typeof TalerMerchantInstanceHttpClient.prototype.updateCurrentInstance,
): VNode {
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
const { state } = useSessionContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />
+ }
+ if (result.type === "fail") {
+ switch(result.case) {
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ default: {
+ assertUnreachable(result)
+ }
+ }
}
return (
@@ -98,7 +94,7 @@ function CommonUpdate(
<UpdatePage
onBack={onBack}
isLoading={false}
- selected={result.data}
+ selected={result.body}
onUpdate={(
d: TalerMerchantApi.InstanceReconfigurationMessage,
): Promise<void> => {
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
index 42a432cf0..70f246ff1 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx
@@ -19,14 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { TalerMerchantApi } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../../components/menu/index.js";
-import { useWebhookAPI } from "../../../../hooks/webhooks.js";
+import { useSessionContext } from "../../../../context/session.js";
import { Notification } from "../../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
-import { TalerMerchantApi } from "@gnu-taler/taler-util";
export type Entity = TalerMerchantApi.WebhookAddDetails;
interface Props {
@@ -35,9 +35,10 @@ interface Props {
}
export default function CreateWebhook({ onConfirm, onBack }: Props): VNode {
- const { createWebhook } = useWebhookAPI();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
return (
<>
@@ -45,7 +46,7 @@ export default function CreateWebhook({ onConfirm, onBack }: Props): VNode {
<CreatePage
onBack={onBack}
onCreate={(request: TalerMerchantApi.WebhookAddDetails) => {
- return createWebhook(request)
+ return lib.instance.addWebhook(state.token, request)
.then(() => onConfirm())
.catch((error) => {
setNotif({
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
index 98bd61d8f..3f1feb8e9 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx
@@ -55,9 +55,7 @@ export function ListPage({
onDelete={onDelete}
onSelect={onSelect}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreBefore={!onLoadMoreBefore}
onLoadMoreAfter={onLoadMoreAfter}
- hasMoreAfter={!onLoadMoreAfter}
/>
</section>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
index 2cafc7f9a..919285e78 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx
@@ -32,8 +32,6 @@ interface Props {
onSelect: (e: Entity) => void;
onCreate: () => void;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -44,8 +42,6 @@ export function CardTable({
onSelect,
onLoadMoreAfter,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
@@ -85,8 +81,6 @@ export function CardTable({
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
- hasMoreAfter={hasMoreAfter}
- hasMoreBefore={hasMoreBefore}
/>
) : (
<EmptyTable />
@@ -104,8 +98,6 @@ interface TableProps {
onSelect: (e: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
- hasMoreBefore?: boolean;
- hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
@@ -115,19 +107,17 @@ function Table({
onDelete,
onSelect,
onLoadMoreBefore,
- hasMoreAfter,
- hasMoreBefore,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
return (
<div class="table-container">
- {hasMoreBefore && (
+ {onLoadMoreBefore && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more webhooks before the first one`}
onClick={onLoadMoreBefore}
>
- <i18n.Translate>load newer webhooks</i18n.Translate>
+ <i18n.Translate>load first page</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
@@ -181,13 +171,13 @@ function Table({
})}
</tbody>
</table>
- {hasMoreAfter && (
+ {onLoadMoreAfter && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more webhooks after the last one`}
onClick={onLoadMoreAfter}
>
- <i18n.Translate>load older webhooks</i18n.Translate>
+ <i18n.Translate>load next page</i18n.Translate>
</button>
)}
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
index 17e767337..789b8d73b 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx
@@ -20,56 +20,54 @@
*/
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ HttpStatusCode,
+ TalerError,
+ TalerMerchantApi,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
-import {
- useInstanceWebhooks,
- useWebhookAPI,
-} from "../../../../hooks/webhooks.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceWebhooks } from "../../../../hooks/webhooks.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { ListPage } from "./ListPage.js";
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
interface Props {
- onUnauthorized: () => VNode;
- onLoadError: (error: HttpError<TalerErrorDetail>) => VNode;
- onNotFound: () => VNode;
onCreate: () => void;
onSelect: (id: string) => void;
}
-export default function ListWebhooks({
- onUnauthorized,
- onLoadError,
- onCreate,
- onSelect,
- onNotFound,
-}: Props): VNode {
- const [position, setPosition] = useState<string | undefined>(undefined);
+export default function ListWebhooks({ onCreate, onSelect }: Props): VNode {
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { deleteWebhook } = useWebhookAPI();
- const result = useInstanceWebhooks({ position }, (id) => setPosition(id));
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
+ const result = useInstanceWebhooks();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
return (
@@ -77,17 +75,16 @@ export default function ListWebhooks({
<NotificationCard notification={notif} />
<ListPage
- webhooks={result.data.webhooks}
- onLoadMoreBefore={
- result.isReachingStart ? result.loadMorePrev : undefined
- }
- onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ webhooks={result.body.webhooks}
+ onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst}
+ onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext}
onCreate={onCreate}
onSelect={(e) => {
onSelect(e.webhook_id);
}}
- onDelete={(e: TalerMerchantApi.WebhookEntry) =>
- deleteWebhook(e.webhook_id)
+ onDelete={(e: TalerMerchantApi.WebhookEntry) => {
+ return lib.instance
+ .deleteWebhook(state.token, e.webhook_id)
.then(() =>
setNotif({
message: i18n.str`webhook delete successfully`,
@@ -100,8 +97,8 @@ export default function ListWebhooks({
type: "ERROR",
description: error.message,
}),
- )
- }
+ );
+ }}
/>
</Fragment>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
index 97b4f44ba..5b2ba7bb9 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx
@@ -19,70 +19,69 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util";
import {
- ErrorType,
- HttpError,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
+import { useSessionContext } from "../../../../context/session.js";
import {
- useWebhookAPI,
useWebhookDetails,
} from "../../../../hooks/webhooks.js";
import { Notification } from "../../../../utils/types.js";
+import { LoginPage } from "../../../login/index.js";
+import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { UpdatePage } from "./UpdatePage.js";
-import { HttpStatusCode, TalerErrorDetail, TalerMerchantApi } from "@gnu-taler/taler-util";
export type Entity = TalerMerchantApi.WebhookPatchDetails & WithId;
interface Props {
onBack?: () => void;
onConfirm: () => void;
- onUnauthorized: () => VNode;
- onNotFound: () => VNode;
- onLoadError: (e: HttpError<TalerErrorDetail>) => VNode;
tid: string;
}
export default function UpdateWebhook({
tid,
onConfirm,
onBack,
- onUnauthorized,
- onNotFound,
- onLoadError,
}: Props): VNode {
- const { updateWebhook } = useWebhookAPI();
+ const { lib } = useSessionContext();
+ const { state } = useSessionContext();
const result = useWebhookDetails(tid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- if (result.loading) return <Loading />;
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- )
- return onUnauthorized();
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.NotFound
- )
- return onNotFound();
- return onLoadError(result);
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <ErrorLoadingMerchant error={result} />;
+ }
+ if (result.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.NotFound: {
+ return <NotFoundPageOrAdminCreate />;
+ }
+ case HttpStatusCode.Unauthorized: {
+ return <LoginPage />
+ }
+ default: {
+ assertUnreachable(result);
+ }
+ }
}
return (
<Fragment>
<NotificationCard notification={notif} />
<UpdatePage
- webhook={{ ...result.data, id: tid }}
+ webhook={{ ...result.body, id: tid }}
onBack={onBack}
onUpdate={(data) => {
- return updateWebhook(tid, data)
+ return lib.instance.updateWebhook(state.token, tid, data)
.then(onConfirm)
.catch((error) => {
setNotif({
diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
index 6a698186a..d77bc75fd 100644
--- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
@@ -19,22 +19,17 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { HttpStatusCode, createRFC8959AccessTokenEncoded } from "@gnu-taler/taler-util";
import {
- HttpStatusCode
-} from "@gnu-taler/taler-util";
-import {
- useMerchantApiContext,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../components/menu/index.js";
-import {
- useSessionContext
-} from "../../context/session.js";
+import { useSessionContext } from "../../context/session.js";
import { Notification } from "../../utils/types.js";
-interface Props { }
+interface Props {}
const tokenRequest = {
scope: "write",
@@ -48,45 +43,18 @@ export function LoginPage(_p: Props): VNode {
const [token, setToken] = useState("");
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { state, logIn } = useSessionContext();
- const { lib } = useMerchantApiContext();
+ const { lib } = useSessionContext();
const { i18n } = useTranslationContext();
- async function doImpersonateImpl(instanceId: string) {
- const result = await lib
- .impersonate(instanceId)
- .createAccessTokenBearer(token, tokenRequest);
- if (result.type === "ok") {
- const { token } = result.body;
- logIn({ token });
- return;
- } else {
- switch (result.case) {
- case HttpStatusCode.Unauthorized: {
- setNotif({
- message: "Your password is incorrect",
- type: "ERROR",
- });
- return;
- }
- case HttpStatusCode.NotFound: {
- setNotif({
- message: "Your instance not found",
- type: "ERROR",
- });
- return;
- }
- }
- }
- }
async function doLoginImpl() {
const result = await lib.authenticate.createAccessTokenBearer(
- token,
+ createRFC8959AccessTokenEncoded(token),
tokenRequest,
);
if (result.type === "ok") {
const { token } = result.body;
- logIn({ token });
+ logIn(token);
return;
} else {
switch (result.case) {
@@ -108,72 +76,6 @@ export function LoginPage(_p: Props): VNode {
}
}
- if (state.status === "loggedIn" && state.impersonate !== undefined) {
- //the user is loggedin but trying to do an impersonation
- return (
- <div class="columns is-centered" style={{ margin: "auto" }}>
- <div class="column is-two-thirds ">
- <div class="modal-card" style={{ width: "100%", margin: 0 }}>
- <header
- class="modal-card-head"
- style={{ border: "1px solid", borderBottom: 0 }}
- >
- <p class="modal-card-title">{i18n.str`Login required`}</p>
- </header>
- <section
- class="modal-card-body"
- style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
- >
- <p>
- <i18n.Translate>
- Need the access token for the instance.
- </i18n.Translate>
- </p>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Access Token</i18n.Translate>
- </label>
- </div>
- <div class="field-body">
- <div class="field">
- <p class="control is-expanded">
- <input
- class="input"
- type="password"
- placeholder={"current access token"}
- name="token"
- onKeyPress={(e) =>
- e.keyCode === 13
- ? doImpersonateImpl(state.instance)
- : null
- }
- value={token}
- onInput={(e): void => setToken(e?.currentTarget.value)}
- />
- </p>
- </div>
- </div>
- </div>
- </section>
- <footer
- class="modal-card-foot "
- style={{
- justifyContent: "flex-end",
- border: "1px solid",
- borderTop: 0,
- }}
- >
- <AsyncButton onClick={() => doImpersonateImpl(state.instance)}>
- <i18n.Translate>Confirm</i18n.Translate>
- </AsyncButton>
- </footer>
- </div>
- </div>
- </div>
- );
- }
-
return (
<Fragment>
<NotificationCard notification={notif} />
@@ -190,7 +92,9 @@ export function LoginPage(_p: Props): VNode {
class="modal-card-body"
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
>
- <i18n.Translate>Please enter your access token.</i18n.Translate>
+ <i18n.Translate>
+ Please enter your access token for <b>"{state.instance}"</b>.
+ </i18n.Translate>
<div class="field is-horizontal">
<div class="field-label is-normal">
diff --git a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
index 68adb79bf..4d348c02b 100644
--- a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
@@ -19,10 +19,19 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode } from "preact";
-import { Link } from "preact-router";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Link, route } from "preact-router";
+import { NotificationCard } from "../../components/menu/index.js";
+import {
+ cleanAllCache,
+ DEFAULT_ADMIN_USERNAME,
+ useSessionContext,
+} from "../../context/session.js";
+import InstanceCreatePage from "../../paths/admin/create/index.js";
+import { InstancePaths } from "../../Routing.js";
-export default function NotFoundPage(): VNode {
+export function NotFoundPage(): VNode {
return (
<div>
<p>That page doesn&apos;t exist.</p>
@@ -32,3 +41,32 @@ export default function NotFoundPage(): VNode {
</div>
);
}
+
+export function NotFoundPageOrAdminCreate(): VNode {
+ const { state } = useSessionContext();
+ const { i18n } = useTranslationContext();
+ if (state.isAdmin && state.instance === DEFAULT_ADMIN_USERNAME) {
+ return (
+ <Fragment>
+ <NotificationCard
+ notification={{
+ message: i18n.str`No 'default' instance configured yet.`,
+ description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`,
+ type: "INFO",
+ }}
+ />
+ <InstanceCreatePage
+ forceId={DEFAULT_ADMIN_USERNAME}
+ onConfirm={() => {
+ // we need to clear everything since we take some
+ // 404 as "default instance don't exist"
+ cleanAllCache()
+ route(InstancePaths.bank_list);
+ }}
+ />
+ </Fragment>
+ );
+ }
+
+ return <NotFoundPage />
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
index 6290f48e6..0c4b9dd1a 100644
--- a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
@@ -45,6 +45,7 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode {
const next = s(value);
const v: Preferences = {
advanceOrderMode: next.advanceOrderMode ?? false,
+ hideMissingAccountUntil: next.hideMissingAccountUntil ?? AbsoluteTime.never(),
hideKycUntil: next.hideKycUntil ?? AbsoluteTime.never(),
dateFormat: next.dateFormat ?? "ymd",
};
diff --git a/packages/merchant-backoffice-ui/src/scss/toggle.scss b/packages/merchant-backoffice-ui/src/scss/toggle.scss
index 24636da2f..6c7346eb3 100644
--- a/packages/merchant-backoffice-ui/src/scss/toggle.scss
+++ b/packages/merchant-backoffice-ui/src/scss/toggle.scss
@@ -4,6 +4,7 @@ $green: #56c080;
cursor: pointer;
display: inline-block;
}
+
.toggle-switch {
display: inline-block;
background: #ccc;
@@ -13,10 +14,12 @@ $green: #56c080;
position: relative;
vertical-align: middle;
transition: background 0.25s;
+
&:before,
&:after {
content: "";
}
+
&:before {
display: block;
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
@@ -29,23 +32,36 @@ $green: #56c080;
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 + & {
+
+ &.disabled:before {
+ background: linear-gradient(to bottom, #ccc 0%, #bbb 100%);
+ }
+
+ .toggle:hover &.disabled:before {
+ background: linear-gradient(to bottom, #ccc 0%, #bbb 100%);
+ }
+
+ .toggle-checkbox:checked+& {
background: $green;
+
&:before {
left: 30px;
}
}
}
+
.toggle-checkbox {
position: absolute;
visibility: hidden;
}
+
.toggle-label {
margin-left: 5px;
position: relative;
top: 2px;
-}
+} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/utils/constants.ts b/packages/merchant-backoffice-ui/src/utils/constants.ts
index 9e7a69ed0..6b4d8eade 100644
--- a/packages/merchant-backoffice-ui/src/utils/constants.ts
+++ b/packages/merchant-backoffice-ui/src/utils/constants.ts
@@ -35,11 +35,10 @@ export const CROCKFORD_BASE32_REGEX =
export const URL_REGEX =
/^((https?:)(\/\/\/?)([\w]*(?::[\w]*)?@)?([\d\w\.-]+)(?::(\d+))?)\/$/;
-// how much rows we add every time user hit load more
-export const PAGE_SIZE = 20;
-// how bigger can be the result set
-// after this threshold, load more with move the cursor
-export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
+export const PAGINATED_LIST_SIZE = 5;
+// when doing paginated request, ask for one more
+// and use it to know if there are more to request
+export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
// how much we will wait for all request, in seconds
export const DEFAULT_REQUEST_TIMEOUT = 10;
diff --git a/packages/pogen/package.json b/packages/pogen/package.json
index 5e601c4ca..24edc348b 100644
--- a/packages/pogen/package.json
+++ b/packages/pogen/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/pogen",
- "version": "0.0.5",
+ "version": "0.10.7",
"bin": {
"pogen": "bin/pogen"
},
diff --git a/packages/pogen/src/potextract.ts b/packages/pogen/src/potextract.ts
index 3e9a95ded..243d44c6f 100644
--- a/packages/pogen/src/potextract.ts
+++ b/packages/pogen/src/potextract.ts
@@ -171,12 +171,12 @@ function processFile(
}
function formatMsgLine(head: string, msg: string) {
+ const m = msg.match(/(.*\n|.+$)/g)
+ if (!m) return;
// Do escaping, wrap break at newlines
console.log("head", JSON.stringify(head));
console.log("msg", JSON.stringify(msg));
- let parts = msg
- .match(/(.*\n|.+$)/g)
- .map((x) => x.replace(/\n/g, "\\n").replace(/"/g, '\\"'))
+ let parts = m.map((x) => x.replace(/\n/g, "\\n").replace(/"/g, '\\"'))
.map((p) => wordwrap(p))
.reduce((a, b) => a.concat(b));
if (parts.length == 1) {
diff --git a/packages/taler-harness/Makefile b/packages/taler-harness/Makefile
index 9abc7b6b0..dea37ec7b 100644
--- a/packages/taler-harness/Makefile
+++ b/packages/taler-harness/Makefile
@@ -34,12 +34,10 @@ install-nodeps:
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/
- install ./bin/create_merchantAndBankAccount_pdf.sh $(DESTDIR)$(NODEDIR)/bin/
- install ./bin/pdf-template.html $(DESTDIR)$(NODEDIR)/bin/
ln -sf ../lib/taler-harness/node_modules/taler-harness/bin/taler-harness.mjs $(DESTDIR)$(BINDIR)/taler-harness
- ln -sf $(DESTDIR)$(NODEDIR)/bin/create_merchantAndBankAccount_pdf.sh $(DESTDIR)$(BINDIR)/create_merchantAndBankAccount_pdf.sh
deps:
pnpm install --frozen-lockfile --filter @gnu-taler/taler-harness...
+ pnpm run --filter @gnu-taler/taler-harness... compile
install:
$(MAKE) deps
$(MAKE) install-nodeps
diff --git a/packages/taler-harness/bin/create_merchantAndBankAccount_pdf.sh b/packages/taler-harness/bin/create_merchantAndBankAccount_pdf.sh
deleted file mode 100755
index cd87c18c9..000000000
--- a/packages/taler-harness/bin/create_merchantAndBankAccount_pdf.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/bash
-THIS_FILE=$(realpath "$0")
-DIR=$(dirname "$THIS_FILE")
-
-DATA=$(mktemp)
-set -e
-
-[ -z "$1" ] && echo First parameter must be the json file result from \'taler-harness deployment provision-bank-and-merchant\'. Alternative \'-\' can be used if the file is provided from stdin. && exit 1
-
-cat $1 > $DATA
-
-[ -z "$(jq -r '.bankUser//empty' $DATA)" ] && echo the json file is not complete: missing bankUser && exit 1
-[ -z "$(jq -r '.bankURL//empty' $DATA)" ] && echo the json file is not complete: missing bankURL && exit 1
-[ -z "$(jq -r '.merchantURL//empty' $DATA)" ] && echo the json file is not complete: missing merchantURL && exit 1
-[ -z "$(jq -r '.templateURI//empty' $DATA)" ] && echo the json file is not complete: missing templateURI && exit 1
-[ -z "$(jq -r '.password//empty' $DATA)" ] && echo the json file is not complete: missing password && exit 1
-
-add_qr_image(){
- jq -r $1 $DATA | qrencode -l Q -m 2 -s 5 -o - | base64 -w 0 | jq -Rn '{"'$2'":inputs}' | jq -s add - $DATA | sponge $DATA
-}
-
-add_qr_image .templateURI templateQR
-add_qr_image .bankURL bankQR
-add_qr_image .merchantURL merchantQR
-
-chevron $DIR/pdf-template.html -d $DATA | wkhtmltopdf - out.pdf
-
diff --git a/packages/taler-harness/bin/pdf-template.html b/packages/taler-harness/bin/pdf-template.html
deleted file mode 100644
index d308d67c4..000000000
--- a/packages/taler-harness/bin/pdf-template.html
+++ /dev/null
@@ -1,65 +0,0 @@
-<html>
-
-<body style="padding: 3em">
- <h1 id="account-information">Account information</h1>
-
- <p>The information in this page is confidentail, do not share with others.</p>
-
- <h2 style="margin-top: 4em;" id="bank-account">Bank</h2>
- <p>
- In your bank account you will be able to see how much revenue
- has been consolidated.
- </p>
- <div style="display: flex; justify-content: space-between;">
-
- <div>
- <p>URL: {{bankURL}}</p>
- <p>accounts id: {{bankUser}}</p>
- <p>password: {{password}}</p>
- </div>
-
- <div>
- <figure style="text-align: center;">
- <img src="data:image/png;base64,{{bankQR}}" alt="" />
- <figcaption>bank URL</figcaption>
- </figure>
- </div>
- </div>
-
- <hr />
-
- <h2 style="margin-top: 4em;" id="merchant-instance">Backoffice</h2>
- <p>
- In this site you will be able to see how much are you selling,
- make refunds or create new QR codes.
- </p>
-
- <div style="display: flex; justify-content: space-between;">
- <div>
- <p>URL: {{merchantURL}}</p>
- <p>password: {{password}}</p>
- </div>
- <div>
- <figure style="text-align: center;">
- <img src="data:image/png;base64,{{merchantQR}}" alt="" />
- <figcaption>merchant URL</figcaption>
- </figure>
- </div>
- </div>
-
- <hr />
- <div style="page-break-after: always;">
-
- </div>
- <h1 style="margin-top: 4em;" id="template">Payme QR code</h1>
- <p>
- The following QR code can be utilized in
- public settings to request payments.
- </p>
- <figure style="text-align: center;">
- <img src="data:image/png;base64,{{templateQR}}" alt="" />
- <figcaption>{{templateURI}}</figcaption>
- </figure>
-</body>
-
-</html> \ No newline at end of file
diff --git a/packages/taler-harness/debian/changelog b/packages/taler-harness/debian/changelog
index 0aa9ccfbd..269c6b99d 100644
--- a/packages/taler-harness/debian/changelog
+++ b/packages/taler-harness/debian/changelog
@@ -1,3 +1,27 @@
+taler-harness (0.10.7) unstable; urgency=low
+
+ * Release 0.10.7
+
+ -- Florian Dold <dold@taler.net> Mon, 22 Apr 2024 20:16:39 +0200
+
+taler-harness (0.10.6) unstable; urgency=low
+
+ * Release 0.10.6
+
+ -- Florian Dold <dold@taler.net> Wed, 10 Apr 2024 15:19:33 +0200
+
+taler-harness (0.10.5) unstable; urgency=low
+
+ * Release 0.10.5
+
+ -- Florian Dold <dold@taler.net> Tue, 09 Apr 2024 16:13:58 +0200
+
+taler-harness (0.10.4) unstable; urgency=low
+
+ * Release 0.10.4
+
+ -- Florian Dold <dold@taler.net> Tue, 09 Apr 2024 14:39:44 +0200
+
taler-harness (0.9.4a) unstable; urgency=low
* Release v0.9.4a.
diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json
index 99cd14861..38d640f51 100644
--- a/packages/taler-harness/package.json
+++ b/packages/taler-harness/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-harness",
- "version": "0.10.0",
+ "version": "0.10.7",
"description": "",
"engines": {
"node": ">=0.12.0"
diff --git a/packages/taler-harness/src/bench1.ts b/packages/taler-harness/src/bench1.ts
index 428114e0e..d260ea731 100644
--- a/packages/taler-harness/src/bench1.ts
+++ b/packages/taler-harness/src/bench1.ts
@@ -29,7 +29,6 @@ import {
} from "@gnu-taler/taler-util";
import {
AccessStats,
- applyRunConfigDefaults,
createNativeWalletHost2,
Wallet,
WalletApiOperation,
@@ -75,7 +74,7 @@ export async function runBench1(configJson: any): Promise<void> {
// my assumption is that the in-memory db file gets too large
if (i % restartWallet == 0) {
if (Object.keys(wallet).length !== 0) {
- wallet.stop();
+ await wallet.client.call(WalletApiOperation.Shutdown, {});
console.log("wallet DB stats", j2s(getDbStats!()));
}
@@ -105,9 +104,7 @@ export async function runBench1(configJson: any): Promise<void> {
exchangeBaseUrl: b1conf.exchange,
});
- await wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
logger.info(
`Finished withdrawal amount=${withdrawAmount} time=${Date.now() - start}`,
@@ -123,16 +120,14 @@ export async function runBench1(configJson: any): Promise<void> {
depositPaytoUri: b1conf.payto,
});
- await wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
logger.info(`Finished deposit amount=10 time=${Date.now() - start}`);
}
}
}
- wallet.stop();
+ await wallet.client.call(WalletApiOperation.Shutdown, {});
console.log("wallet DB stats", j2s(getDbStats!()));
}
diff --git a/packages/taler-harness/src/bench3.ts b/packages/taler-harness/src/bench3.ts
index f138dff68..ddf763c5b 100644
--- a/packages/taler-harness/src/bench3.ts
+++ b/packages/taler-harness/src/bench3.ts
@@ -85,7 +85,7 @@ export async function runBench3(configJson: any): Promise<void> {
// my assumption is that the in-memory db file gets too large
if (i % restartWallet == 0) {
if (Object.keys(wallet).length !== 0) {
- wallet.stop();
+ await wallet.client.call(WalletApiOperation.Shutdown, {});
console.log("wallet DB stats", j2s(getDbStats!()));
}
@@ -115,9 +115,7 @@ export async function runBench3(configJson: any): Promise<void> {
exchangeBaseUrl: b3conf.exchange,
});
- await wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
logger.info(
`Finished withdrawal amount=${withdrawAmount} time=${Date.now() - start}`,
@@ -135,15 +133,13 @@ export async function runBench3(configJson: any): Promise<void> {
depositPaytoUri: payto,
});
- await wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
logger.info(`Finished deposit amount=10 time=${Date.now() - start}`);
}
}
- wallet.stop();
+ await wallet.client.call(WalletApiOperation.Shutdown, {});
console.log("wallet DB stats", j2s(getDbStats!()));
}
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 68c0744fc..b27eaa371 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -651,7 +651,7 @@ export class FakebankService
config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`);
config.setString("bank", "ram_limit", `${1024}`);
const cfgFilename = testDir + "/bank.conf";
- config.write(cfgFilename, { excludeDefaults: true });
+ config.writeTo(cfgFilename, { excludeDefaults: true });
return new FakebankService(gc, bc, cfgFilename);
}
@@ -680,7 +680,7 @@ export class FakebankService
}
const config = Configuration.load(this.configFile);
config.setString("bank", "suggested_exchange", e.baseUrl);
- config.write(this.configFile, { excludeDefaults: true });
+ config.writeTo(this.configFile, { excludeDefaults: true });
}
get baseUrl(): string {
@@ -790,7 +790,7 @@ export class LibeufinBankService
`${bc.currency}:100`,
);
const cfgFilename = testDir + "/bank.conf";
- config.write(cfgFilename, { excludeDefaults: true });
+ config.writeTo(cfgFilename, { excludeDefaults: true });
return new LibeufinBankService(gc, bc, cfgFilename);
}
@@ -828,7 +828,7 @@ export class LibeufinBankService
"suggested_withdrawal_exchange",
e.baseUrl,
);
- config.write(this.configFile, { excludeDefaults: true });
+ config.writeTo(this.configFile, { excludeDefaults: true });
}
get baseUrl(): string {
@@ -1052,7 +1052,7 @@ export class ExchangeService implements ExchangeServiceInterface {
changeConfig(f: (config: Configuration) => void) {
const config = Configuration.load(this.configFilename);
f(config);
- config.write(this.configFilename, { excludeDefaults: true });
+ config.writeTo(this.configFilename, { excludeDefaults: true });
}
static create(gc: GlobalTestState, e: ExchangeConfig) {
@@ -1118,7 +1118,7 @@ export class ExchangeService implements ExchangeServiceInterface {
fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv));
const cfgFilename = testDir + `/exchange-${e.name}.conf`;
- config.write(cfgFilename, { excludeDefaults: true });
+ config.writeTo(cfgFilename, { excludeDefaults: true });
return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey);
}
@@ -1127,13 +1127,13 @@ export class ExchangeService implements ExchangeServiceInterface {
offeredCoins.forEach((cc) =>
setCoin(config, cc(this.exchangeConfig.currency)),
);
- config.write(this.configFilename, { excludeDefaults: true });
+ config.writeTo(this.configFilename, { excludeDefaults: true });
}
addCoinConfigList(ccs: CoinConfig[]) {
const config = Configuration.load(this.configFilename);
ccs.forEach((cc) => setCoin(config, cc));
- config.write(this.configFilename, { excludeDefaults: true });
+ config.writeTo(this.configFilename, { excludeDefaults: true });
}
enableAgeRestrictions(maskStr: string) {
@@ -1144,7 +1144,7 @@ export class ExchangeService implements ExchangeServiceInterface {
"age_groups",
maskStr,
);
- config.write(this.configFilename, { excludeDefaults: true });
+ config.writeTo(this.configFilename, { excludeDefaults: true });
}
get masterPub() {
@@ -1165,7 +1165,7 @@ export class ExchangeService implements ExchangeServiceInterface {
): Promise<void> {
const config = Configuration.load(this.configFilename);
await f(config);
- config.write(this.configFilename, { excludeDefaults: true });
+ config.writeTo(this.configFilename, { excludeDefaults: true });
}
async addBankAccount(
@@ -1206,7 +1206,7 @@ export class ExchangeService implements ExchangeServiceInterface {
"password",
exchangeBankAccount.accountPassword,
);
- config.write(this.configFilename, { excludeDefaults: true });
+ config.writeTo(this.configFilename, { excludeDefaults: true });
}
exchangeHttpProc: ProcessWrapper | undefined;
@@ -1701,7 +1701,7 @@ export class MerchantService implements MerchantServiceInterface {
config.setString("merchantdb-postgres", "config", mc.database);
// Do not contact demo.taler.net exchange in tests
config.setString("merchant-exchange-kudos", "disabled", "yes");
- config.write(cfgFilename, { excludeDefaults: true });
+ config.writeTo(cfgFilename, { excludeDefaults: true });
return new MerchantService(gc, mc, cfgFilename);
}
@@ -1719,7 +1719,7 @@ export class MerchantService implements MerchantServiceInterface {
this.merchantConfig.currency,
);
config.setString(`merchant-exchange-${e.name}`, "master_key", e.masterPub);
- config.write(this.configFilename, { excludeDefaults: true });
+ config.writeTo(this.configFilename, { excludeDefaults: true });
}
async addDefaultInstance(): Promise<void> {
diff --git a/packages/taler-harness/src/harness/sync.ts b/packages/taler-harness/src/harness/sync.ts
index 64c9acaef..567a2e92d 100644
--- a/packages/taler-harness/src/harness/sync.ts
+++ b/packages/taler-harness/src/harness/sync.ts
@@ -85,7 +85,7 @@ export class SyncService {
config.setString("syncdb-postgres", "config", sc.database);
config.setString("sync", "payment_backend_url", sc.paymentBackendUrl);
config.setString("sync", "upload_limit_mb", `${sc.uploadLimitMb}`);
- config.write(cfgFilename);
+ config.writeTo(cfgFilename);
return new SyncService(gc, sc, cfgFilename);
}
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
index 4b0319a3e..315173b7f 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -30,10 +30,11 @@ import {
TalerAuthenticationHttpClient,
TalerBankConversionHttpClient,
TalerCoreBankHttpClient,
- TalerErrorCode,
TalerMerchantInstanceHttpClient,
TalerMerchantManagementHttpClient,
TransactionsResponse,
+ createRFC8959AccessTokenEncoded,
+ createRFC8959AccessTokenPlain,
decodeCrock,
encodeCrock,
generateIban,
@@ -41,8 +42,7 @@ import {
randomBytes,
rsaBlind,
setGlobalLogLevelFromString,
- setPrintHttpRequestAsCurl,
- stringifyPayTemplateUri
+ stringifyPayTemplateUri,
} from "@gnu-taler/taler-util";
import { clk } from "@gnu-taler/taler-util/clk";
import {
@@ -56,7 +56,7 @@ import {
} from "@gnu-taler/taler-wallet-core";
import {
downloadExchangeInfo,
- topupReserveWithDemobank,
+ topupReserveWithBank,
} from "@gnu-taler/taler-wallet-core/dbless";
import { deepStrictEqual } from "assert";
import fs from "fs";
@@ -79,7 +79,6 @@ import {
} from "./harness/helpers.js";
import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
import { lintExchangeDeployment } from "./lint.js";
-import { randomUUID } from "crypto";
const logger = new Logger("taler-harness:index.ts");
@@ -355,25 +354,46 @@ advancedCli
);
});
-const configCli = testingCli.subcommand("configArgs", "config", {
- help: "Subcommands for handling the Taler configuration.",
-});
+const configCli = testingCli
+ .subcommand("configArgs", "config", {
+ help: "Subcommands for handling the Taler configuration.",
+ })
+ .maybeOption("configEntryFile", ["-c", "--config"], clk.STRING, {
+ help: "Configuration file to use.",
+ })
+ .maybeOption("project", ["--project"], clk.STRING, {
+ help: `Selection of the project to inspect/change the config (default: taler).`,
+ });
-configCli.subcommand("show", "show").action(async (args) => {
- const config = Configuration.load();
- const cfgStr = config.stringify({
- diagnostics: true,
+configCli
+ .subcommand("show", "show", {
+ help: "Show the current configuration.",
+ })
+ .action(async (args) => {
+ const config = Configuration.load(
+ args.configArgs.configEntryFile,
+ args.configArgs.project,
+ );
+ const cfgStr = config.stringify({
+ diagnostics: true,
+ });
+ console.log(cfgStr);
});
- console.log(cfgStr);
-});
configCli
- .subcommand("get", "get")
+ .subcommand("get", "get", {
+ help: "Get a configuration option.",
+ })
.requiredArgument("section", clk.STRING)
.requiredArgument("option", clk.STRING)
- .flag("file", ["-f"])
+ .flag("file", ["-f"], {
+ help: "Treat the value as a filename, expanding placeholders.",
+ })
.action(async (args) => {
- const config = Configuration.load();
+ const config = Configuration.load(
+ args.configArgs.configEntryFile,
+ args.configArgs.project,
+ );
let res;
if (args.get.file) {
res = config.getPath(args.get.section, args.get.option);
@@ -388,6 +408,35 @@ configCli
}
});
+configCli
+ .subcommand("set", "set", {
+ help: "Set a configuration option.",
+ })
+ .requiredArgument("section", clk.STRING)
+ .requiredArgument("option", clk.STRING)
+ .requiredArgument("value", clk.STRING)
+ .flag("dry", ["--dry"], {
+ help: "Do not write the changed config to disk, only write it to stdout.",
+ })
+ .action(async (args) => {
+ const config = Configuration.load(
+ args.configArgs.configEntryFile,
+ args.configArgs.project,
+ );
+ config.setString(args.set.section, args.set.option, args.set.value);
+ if (args.set.dry) {
+ console.log(
+ config.stringify({
+ excludeDefaults: true,
+ }),
+ );
+ } else {
+ config.write({
+ excludeDefaults: true,
+ });
+ }
+ });
+
const deploymentCli = testingCli.subcommand("deploymentArgs", "deployment", {
help: "Subcommands for handling GNU Taler deployments.",
});
@@ -403,7 +452,7 @@ deploymentCli
const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
const exchangeBaseUrl = "https://exchange.demo.taler.net/";
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
- await topupReserveWithDemobank({
+ await topupReserveWithBank({
amount: "KUDOS:10" as AmountString,
corebankApiBaseUrl: "https://bank.demo.taler.net/",
exchangeInfo,
@@ -431,7 +480,7 @@ deploymentCli
const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
const exchangeBaseUrl = "https://exchange.test.taler.net/";
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
- await topupReserveWithDemobank({
+ await topupReserveWithBank({
amount: "TESTKUDOS:10" as AmountString,
corebankApiBaseUrl: "https://bank.test.taler.net/",
exchangeInfo,
@@ -460,7 +509,7 @@ deploymentCli
const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
const exchangeBaseUrl = "http://localhost:8081/";
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
- await topupReserveWithDemobank({
+ await topupReserveWithBank({
amount: "TESTKUDOS:10" as AmountString,
corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
exchangeInfo,
@@ -601,65 +650,99 @@ deploymentCli
help: "Provision a bank account, merchant instance and link them together.",
})
.requiredArgument("merchantApiBaseUrl", clk.STRING, {
- help: "URL location of the merchant backend"
+ help: "URL location of the merchant backend",
})
.requiredArgument("corebankApiBaseUrl", clk.STRING, {
- help: "URL location of the libeufin bank backend"
- })
- .requiredOption("merchantToken", ["--merchant-management-token"], clk.STRING, {
- help: "acces token of the default instance in the merchant backend"
+ help: "URL location of the libeufin bank backend",
})
+ .requiredOption(
+ "merchantToken",
+ ["--merchant-management-token"],
+ clk.STRING,
+ {
+ help: "access token of the default instance in the merchant backend",
+ },
+ )
.maybeOption("bankToken", ["--bank-admin-token"], clk.STRING, {
- help: "libeufin bank admin's password if the account creation is restricted"
+ help: "libeufin bank admin's token if the account creation is restricted",
+ })
+ .maybeOption("bankPassword", ["--bank-admin-password"], clk.STRING, {
+ help: "libeufin bank admin's password if the account creation is restricted, it will override --bank-admin-token",
})
.requiredOption("name", ["--legal-name"], clk.STRING, {
- help: "legal name of the merchant"
+ help: "legal name of the merchant",
})
.maybeOption("email", ["--email"], clk.STRING, {
- help: "email contact of the merchant"
+ help: "email contact of the merchant",
})
.maybeOption("phone", ["--phone"], clk.STRING, {
- help: "phone contact of the merchant"
+ help: "phone contact of the merchant",
})
.requiredOption("id", ["--id"], clk.STRING, {
- help: "login id for the bank account and instance id of the merchant backend"
+ help: "login id for the bank account and instance id of the merchant backend",
})
.flag("template", ["--create-template"], {
- help: "use this flag to create a default template for the merchant with fixed summary"
+ help: "use this flag to create a default template for the merchant with fixed summary",
})
.requiredOption("password", ["--password"], clk.STRING, {
- help: "password of the accounts in libeufin bank and merchant backend"
+ help: "password of the accounts in libeufin bank and merchant backend",
})
.flag("randomPassword", ["--set-random-password"], {
- help: "if everything worked ok, change the password of the accounts at the end"
+ help: "if everything worked ok, change the password of the accounts at the end",
})
.action(async (args) => {
- const managementToken = args.provisionBankMerchant.merchantToken as AccessToken;
- const bankAdminPassword = args.provisionBankMerchant.bankToken as AccessToken;
+ const managementToken = createRFC8959AccessTokenPlain(
+ args.provisionBankMerchant.merchantToken,
+ );
+ const bankAdminPassword = args.provisionBankMerchant.bankPassword;
+ const bankAdminTokenArg = args.provisionBankMerchant.bankToken
+ ? createRFC8959AccessTokenPlain(args.provisionBankMerchant.bankToken)
+ : undefined;
const id = args.provisionBankMerchant.id;
const name = args.provisionBankMerchant.name;
const email = args.provisionBankMerchant.email;
const phone = args.provisionBankMerchant.phone;
const password = args.provisionBankMerchant.password;
-
const httpLib = createPlatformHttpLib({});
- const merchantManager = new TalerMerchantManagementHttpClient(args.provisionBankMerchant.merchantApiBaseUrl, httpLib);
- const bank = new TalerCoreBankHttpClient(args.provisionBankMerchant.corebankApiBaseUrl, httpLib);
- const instanceURL = merchantManager.getSubInstanceAPI(id).href
- const merchantInstance = new TalerMerchantInstanceHttpClient(instanceURL, httpLib);
- const conv = new TalerBankConversionHttpClient(bank.getConversionInfoAPI().href, httpLib)
- const bankAuth = new TalerAuthenticationHttpClient(bank.getAuthenticationAPI(id).href, httpLib)
-
+ const merchantManager = new TalerMerchantManagementHttpClient(
+ args.provisionBankMerchant.merchantApiBaseUrl,
+ httpLib,
+ );
+ const bank = new TalerCoreBankHttpClient(
+ args.provisionBankMerchant.corebankApiBaseUrl,
+ httpLib,
+ );
+ const instanceURL = merchantManager.getSubInstanceAPI(id).href;
+ const merchantInstance = new TalerMerchantInstanceHttpClient(
+ instanceURL,
+ httpLib,
+ );
+ const conv = new TalerBankConversionHttpClient(
+ bank.getConversionInfoAPI().href,
+ httpLib,
+ );
+ const bankAuth = new TalerAuthenticationHttpClient(
+ bank.getAuthenticationAPI(id).href,
+ httpLib,
+ );
- const bc = await bank.getConfig()
+ const bc = await bank.getConfig();
+ if (bc.type === "fail") {
+ logger.error(`couldn't get bank config. ${bc.detail.hint}`);
+ return;
+ }
if (!bank.isCompatible(bc.body.version)) {
logger.error(
`bank server version is not compatible: ${bc.body.version}, client version: ${bank.PROTOCOL_VERSION}`,
);
return;
}
- const mc = await merchantManager.getConfig()
+ const mc = await merchantManager.getConfig();
+ if (mc.type === "fail") {
+ logger.error(`couldn't get merchant config. ${mc.detail.hint}`);
+ return;
+ }
if (!merchantManager.isCompatible(mc.body.version)) {
logger.error(
`merchant server version is not compatible: ${mc.body.version}, client version: ${merchantManager.PROTOCOL_VERSION}`,
@@ -667,27 +750,59 @@ deploymentCli
return;
}
+ let bankAdminToken: AccessToken | undefined;
+ if (bankAdminPassword) {
+ const adminAuth = new TalerAuthenticationHttpClient(
+ bank.getAuthenticationAPI("admin").href,
+ httpLib,
+ );
+
+ const resp = await adminAuth.createAccessTokenBasic(
+ "admin",
+ bankAdminPassword,
+ {
+ scope: "write",
+ duration: {
+ d_us: 1000 * 1000 * 10, //10 secs
+ },
+ refreshable: false,
+ },
+ );
+ if (resp.type === "fail") {
+ logger.error(`could not get bank admin token from password.`);
+ return;
+ }
+ bankAdminToken = resp.body.access_token;
+ } else {
+ bankAdminToken = bankAdminTokenArg;
+ }
+
/**
* create bank account
*/
let accountPayto: PaytoString;
{
- const resp = await bank.createAccount(bankAdminPassword, {
+ const resp = await bank.createAccount(bankAdminToken, {
name: name,
password: password,
username: id,
- contact_data: email || phone ? {
- email: email,
- phone: phone,
- } : undefined,
- })
+ contact_data:
+ email || phone
+ ? {
+ email: email,
+ phone: phone,
+ }
+ : undefined,
+ });
if (resp.type === "fail") {
- logger.error(`unable to provision bank account, HTTP response status ${resp.case}`);
+ logger.error(
+ `unable to provision bank account, HTTP response status ${resp.case}`,
+ );
process.exit(2);
}
logger.info(`account ${id} successfully provisioned`);
- accountPayto = resp.body.internal_payto_uri
+ accountPayto = resp.body.internal_payto_uri;
}
/**
@@ -698,7 +813,7 @@ deploymentCli
address: {},
auth: {
method: "token",
- token: `secret-token:${password}`,
+ token: createRFC8959AccessTokenPlain(password),
},
default_pay_delay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ hours: 1 }),
@@ -710,7 +825,7 @@ deploymentCli
jurisdiction: {},
name: name,
use_stefan: true,
- })
+ });
if (resp.type === "ok") {
logger.info(`instance ${id} created successfully`);
@@ -729,21 +844,26 @@ deploymentCli
* link bank account and merchant
*/
{
- const resp = await merchantInstance.addAccount(password as AccessToken, {
- payto_uri: accountPayto,
- credit_facade_url: bank.getRevenueAPI(id).href,
- credit_facade_credentials: {
- type: "basic",
- username: id,
- password: password,
- }
- })
+ const resp = await merchantInstance.addBankAccount(
+ createRFC8959AccessTokenEncoded(password),
+ {
+ payto_uri: accountPayto,
+ credit_facade_url: bank.getRevenueAPI(id).href,
+ credit_facade_credentials: {
+ type: "basic",
+ username: id,
+ password: password,
+ },
+ },
+ );
if (resp.type === "fail") {
- console.error(`unable to configure bank account for instance ${id}, status ${resp.case}`)
+ console.error(
+ `unable to configure bank account for instance ${id}, status ${resp.case}`,
+ );
console.error(j2s(resp.detail));
process.exit(2);
}
- wireAccount = resp.body.h_wire
+ wireAccount = resp.body.h_wire;
}
logger.info(`successfully configured bank account for ${id}`);
@@ -757,32 +877,35 @@ deploymentCli
if (bc.body.allow_conversion) {
const cc = await conv.getConfig();
if (cc.type === "ok") {
- currency = cc.body.fiat_currency
+ currency = cc.body.fiat_currency;
} else {
- console.error(
- `could not get fiat currency status ${cc.case}`,
- );
+ console.error(`could not get fiat currency status ${cc.case}`);
console.error(j2s(cc.detail));
}
} else {
- console.log(`conversion is disabled, using bank currency`)
+ console.log(`conversion is disabled, using bank currency`);
}
{
- const resp = await merchantInstance.addTemplate(password as AccessToken, {
- template_id: "default",
- template_description: "First template",
- template_contract: {
- pay_duration: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ hours: 1 }),
- ),
- minimum_age: 0,
- currency,
- summary: "Pay me!"
- }
- })
+ const resp = await merchantInstance.addTemplate(
+ createRFC8959AccessTokenEncoded(password),
+ {
+ template_id: "default",
+ template_description: "First template",
+ template_contract: {
+ pay_duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ minimum_age: 0,
+ currency,
+ summary: "Pay me!",
+ },
+ },
+ );
if (resp.type === "fail") {
- console.error(`unable to create template for insntaince ${id}, status ${resp.case}`)
+ console.error(
+ `unable to create template for insntaince ${id}, status ${resp.case}`,
+ );
console.error(j2s(resp.detail));
process.exit(2);
}
@@ -793,25 +916,29 @@ deploymentCli
merchantBaseUrl: instanceURL,
templateId: "default",
templateParams: {
- amount: currency
- }
- })
+ amount: currency,
+ },
+ });
}
let finalPassword = password;
if (args.provisionBankMerchant.randomPassword) {
- const prevPassword = password as AccessToken
+ const prevPassword = password;
const randomPassword = encodeCrock(randomBytes(16));
- logger.info("random password: ", randomPassword)
+ logger.info("random password: ", randomPassword);
let token: AccessToken;
{
const resp = await bankAuth.createAccessTokenBasic(id, prevPassword, {
scope: "readwrite",
- duration: Duration.toTalerProtocolDuration(Duration.fromSpec({ minutes: 1 })),
+ duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
refreshable: false,
- })
+ });
if (resp.type === "fail") {
- console.error(`unable to login into bank accountfor user ${id}, status ${resp.case}`)
+ console.error(
+ `unable to login into bank accountfor user ${id}, status ${resp.case}`,
+ );
console.error(j2s(resp.detail));
process.exit(2);
}
@@ -819,42 +946,56 @@ deploymentCli
}
{
- const resp = await bank.updatePassword({ username: id, token }, {
- old_password: prevPassword,
- new_password: randomPassword,
- });
+ const resp = await bank.updatePassword(
+ { username: id, token },
+ {
+ old_password: prevPassword,
+ new_password: randomPassword,
+ },
+ );
if (resp.type === "fail") {
- console.error(`unable to change bank pasword for user ${id}, status ${resp.case}`)
+ console.error(
+ `unable to change bank password for user ${id}, status ${resp.case}`,
+ );
if (resp.case !== HttpStatusCode.Accepted) {
console.error(j2s(resp.detail));
} else {
- console.error("2FA required")
+ console.error("2FA required");
}
process.exit(2);
}
}
{
- const resp = await merchantInstance.updateCurrentInstanceAuthentication(prevPassword, {
- method: "token",
- token: `secret-token:${randomPassword}` as AccessToken
- })
+ const resp = await merchantInstance.updateCurrentInstanceAuthentication(
+ createRFC8959AccessTokenEncoded(prevPassword),
+ {
+ method: "token",
+ token: createRFC8959AccessTokenPlain(randomPassword),
+ },
+ );
if (resp.type === "fail") {
- console.error(`unable to change merchant password for instance ${id}, status ${resp.case}`)
+ console.error(
+ `unable to change merchant password for instance ${id}, status ${resp.case}`,
+ );
console.error(j2s(resp.detail));
process.exit(2);
}
}
{
- const resp = await merchantInstance.updateAccount(randomPassword as AccessToken, wireAccount, {
- credit_facade_url: bank.getRevenueAPI(id).href,
- credit_facade_credentials: {
- type: "basic",
- username: id,
- password: randomPassword,
- }
- })
+ const resp = await merchantInstance.updateBankAccount(
+ createRFC8959AccessTokenEncoded(randomPassword),
+ wireAccount,
+ {
+ credit_facade_url: bank.getRevenueAPI(id).href,
+ credit_facade_credentials: {
+ type: "basic",
+ username: id,
+ password: randomPassword,
+ },
+ },
+ );
if (resp.type != "ok") {
console.error(
`unable to update bank account for instance ${id}, status ${resp.case}`,
@@ -870,17 +1011,21 @@ deploymentCli
/**
* show result
*/
- console.log(JSON.stringify({
- bankUser: id,
- bankURL: args.provisionBankMerchant.corebankApiBaseUrl,
- merchantURL: instanceURL,
- templateURI,
- password: finalPassword,
- }, undefined, 2))
-
+ console.log(
+ JSON.stringify(
+ {
+ bankUser: id,
+ bankURL: args.provisionBankMerchant.corebankApiBaseUrl,
+ merchantURL: instanceURL,
+ templateURI,
+ password: finalPassword,
+ },
+ undefined,
+ 2,
+ ),
+ );
});
-
deploymentCli
.subcommand("provisionMerchantInstance", "provision-merchant-instance", {
help: "Provision a merchant backend instance.",
@@ -897,9 +1042,16 @@ deploymentCli
.action(async (args) => {
const httpLib = createPlatformHttpLib({});
const baseUrl = args.provisionMerchantInstance.merchantApiBaseUrl;
- const api = new TalerMerchantManagementHttpClient(baseUrl, httpLib)
- const managementToken = args.provisionMerchantInstance.managementToken as AccessToken;
- const instanceToken = args.provisionMerchantInstance.instanceToken as AccessToken;
+ const api = new TalerMerchantManagementHttpClient(baseUrl, httpLib);
+ const managementToken = createRFC8959AccessTokenEncoded(
+ args.provisionMerchantInstance.managementToken,
+ );
+ const instanceTokenEnc = createRFC8959AccessTokenPlain(
+ args.provisionMerchantInstance.instanceToken,
+ );
+ const instanceTokenPlain = createRFC8959AccessTokenPlain(
+ args.provisionMerchantInstance.instanceToken,
+ );
const instanceId = args.provisionMerchantInstance.id;
const instancceName = args.provisionMerchantInstance.name;
const bankURL = args.provisionMerchantInstance.bankURL;
@@ -911,7 +1063,7 @@ deploymentCli
address: {},
auth: {
method: "token",
- token: `secret-token:${instanceToken}`,
+ token: instanceTokenPlain,
},
default_pay_delay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ hours: 1 }),
@@ -921,7 +1073,7 @@ deploymentCli
jurisdiction: {},
name: instancceName,
use_stefan: true,
- })
+ });
if (createResp.type === "ok") {
logger.info(`instance ${instanceId} created successfully`);
@@ -934,15 +1086,18 @@ deploymentCli
process.exit(2);
}
- const createAccountResp = await api.addAccount(instanceToken, {
+ const createAccountResp = await api.addBankAccount(instanceTokenEnc, {
payto_uri: accountPayto,
credit_facade_url: bankURL,
- credit_facade_credentials: bankUser && bankPassword ? {
- type: "basic",
- username: bankUser,
- password: bankPassword,
- } : undefined
- })
+ credit_facade_credentials:
+ bankUser && bankPassword
+ ? {
+ type: "basic",
+ username: bankUser,
+ password: bankPassword,
+ }
+ : undefined,
+ });
if (createAccountResp.type != "ok") {
console.error(
`unable to configure bank account for instance ${instanceId}, status ${createAccountResp.case}`,
@@ -977,7 +1132,7 @@ deploymentCli
is_public: !!args.provisionBankAccount.public,
is_taler_exchange: !!args.provisionBankAccount.exchange,
payto_uri: args.provisionBankAccount.internalPayto as PaytoString,
- })
+ });
if (resp.type === "ok") {
logger.info(`account ${accountLogin} successfully provisioned`);
@@ -1043,23 +1198,6 @@ deploymentCli
console.log(out);
});
-const deploymentConfigCli = deploymentCli.subcommand("configArgs", "config", {
- help: "Subcommands the Taler configuration.",
-});
-
-deploymentConfigCli
- .subcommand("show", "show")
- .flag("diagnostics", ["-d", "--diagnostics"])
- .maybeArgument("cfgfile", clk.STRING, {})
- .action(async (args) => {
- const cfg = Configuration.load(args.show.cfgfile);
- console.log(
- cfg.stringify({
- diagnostics: args.show.diagnostics,
- }),
- );
- });
-
testingCli.subcommand("logtest", "logtest").action(async (args) => {
logger.trace("This is a trace message.");
logger.info("This is an info message.");
diff --git a/packages/taler-harness/src/integrationtests/test-currency-scope.ts b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
index e07a8f47b..058941e16 100644
--- a/packages/taler-harness/src/integrationtests/test-currency-scope.ts
+++ b/packages/taler-harness/src/integrationtests/test-currency-scope.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import { Duration, j2s } from "@gnu-taler/taler-util";
-import { Wallet, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
@@ -30,7 +30,6 @@ import {
} from "../harness/harness.js";
import {
createWalletDaemonWithClient,
- makeTestPaymentV2,
withdrawViaBankV2,
} from "../harness/helpers.js";
diff --git a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
index 79269d533..6a82d586c 100644
--- a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
+++ b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
@@ -21,6 +21,7 @@ import {
MerchantApiClient,
PreparePayResultType,
TalerErrorCode,
+ TransactionType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
@@ -93,7 +94,7 @@ export async function runDenomUnofferedTest(t: GlobalTestState) {
);
const confirmResp = await walletClient.call(WalletApiOperation.ConfirmPay, {
- proposalId: preparePayResult.proposalId,
+ transactionId: preparePayResult.transactionId,
});
const tx = await walletClient.call(WalletApiOperation.GetTransactionById, {
@@ -147,8 +148,16 @@ export async function runDenomUnofferedTest(t: GlobalTestState) {
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- const txs = await walletClient.call(WalletApiOperation.GetTransactions, {});
+ const txs = await walletClient.call(WalletApiOperation.GetTransactions, {
+ sort: "stable-ascending",
+ includeRefreshes: true,
+ });
console.log(JSON.stringify(txs, undefined, 2));
+
+ t.assertDeepEqual(txs.transactions[0].type, TransactionType.Withdrawal);
+ t.assertDeepEqual(txs.transactions[1].type, TransactionType.Refresh);
+ t.assertDeepEqual(txs.transactions[2].type, TransactionType.DenomLoss);
+ t.assertDeepEqual(txs.transactions[3].type, TransactionType.Withdrawal);
}
runDenomUnofferedTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts b/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts
index 05e6e153b..47a17a1f2 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts
@@ -34,7 +34,7 @@ import {
depositCoin,
downloadExchangeInfo,
findDenomOrThrow,
- topupReserveWithDemobank,
+ topupReserveWithBank,
withdrawCoin,
} from "@gnu-taler/taler-wallet-core/dbless";
import { GlobalTestState } from "../harness/harness.js";
@@ -63,7 +63,7 @@ export async function runExchangeDepositTest(t: GlobalTestState) {
const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
- await topupReserveWithDemobank({
+ await topupReserveWithBank({
http,
amount: "TESTKUDOS:10" as AmountString,
corebankApiBaseUrl: bank.corebankApiBaseUrl,
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
index 83ee13d4e..6666e2d0b 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts
@@ -40,7 +40,7 @@ import {
checkReserve,
downloadExchangeInfo,
findDenomOrThrow,
- topupReserveWithDemobank,
+ topupReserveWithBank,
withdrawCoin,
} from "@gnu-taler/taler-wallet-core/dbless";
import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
@@ -76,7 +76,7 @@ export async function runExchangePurseTest(t: GlobalTestState) {
method: "GET",
});
- await topupReserveWithDemobank({
+ await topupReserveWithBank({
amount: "TESTKUDOS:10" as AmountString,
http,
reservePub: reserveKeyPair.pub,
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
index 74ef64234..714a7f879 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
@@ -38,7 +38,6 @@ import {
GlobalTestState,
MerchantService,
setupDb,
- WalletCli,
} from "../harness/harness.js";
import {
applyTimeTravelV2,
diff --git a/packages/taler-harness/src/integrationtests/test-payment-expired.ts b/packages/taler-harness/src/integrationtests/test-payment-expired.ts
index 176fc74f7..a837b18fa 100644
--- a/packages/taler-harness/src/integrationtests/test-payment-expired.ts
+++ b/packages/taler-harness/src/integrationtests/test-payment-expired.ts
@@ -117,15 +117,16 @@ export async function runPaymentExpiredTest(t: GlobalTestState) {
});
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
- const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
- console.log(bal);
-
- t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:18.93");
-
const txns = await walletClient.call(WalletApiOperation.GetTransactions, {
+ sort: "stable-ascending",
includeRefreshes: true,
});
console.log(j2s(txns));
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(j2s(bal));
+
+ t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:18.93");
}
runPaymentExpiredTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts b/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts
new file mode 100644
index 000000000..6de3c2e33
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-peer-pull-large.ts
@@ -0,0 +1,194 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ j2s,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WalletNotification,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import {
+ BankServiceHandle,
+ ExchangeService,
+ GlobalTestState,
+ WalletClient,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+];
+
+export async function runPeerPullLargeTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ coinConfigList,
+ );
+
+ let allW1Notifications: WalletNotification[] = [];
+ let allW2Notifications: WalletNotification[] = [];
+
+ const w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ handleNotification(wn) {
+ allW1Notifications.push(wn);
+ },
+ });
+ const w2 = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ persistent: true,
+ handleNotification(wn) {
+ allW2Notifications.push(wn);
+ },
+ });
+
+ // Withdraw digital cash into the wallet.
+ const wallet1 = w1.walletClient;
+ const wallet2 = w2.walletClient;
+
+ await checkNormalPeerPull(t, bank, exchange, wallet1, wallet2);
+}
+
+async function checkNormalPeerPull(
+ t: GlobalTestState,
+ bank: BankServiceHandle,
+ exchange: ExchangeService,
+ wallet1: WalletClient,
+ wallet2: WalletClient,
+): Promise<void> {
+ const withdrawRes = await withdrawViaBankV2(t, {
+ walletClient: wallet2,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:500",
+ });
+
+ await withdrawRes.withdrawalFinishedCond;
+
+ const purseExpiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const resp = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPullCredit,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:200" as AmountString,
+ purse_expiration: purseExpiration,
+ },
+ },
+ );
+
+ const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready,
+ );
+
+ await peerPullCreditReadyCond;
+
+ const creditTx = await wallet1.call(WalletApiOperation.GetTransactionById, {
+ transactionId: resp.transactionId,
+ });
+
+ t.assertDeepEqual(creditTx.type, TransactionType.PeerPullCredit);
+ t.assertTrue(!!creditTx.talerUri);
+
+ const checkResp = await wallet2.client.call(
+ WalletApiOperation.PreparePeerPullDebit,
+ {
+ talerUri: creditTx.talerUri,
+ },
+ );
+
+ console.log(`checkResp: ${j2s(checkResp)}`);
+
+ const peerPullCreditDoneCond = wallet1.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === resp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Done,
+ );
+
+ const peerPullDebitDoneCond = wallet2.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.transactionId === checkResp.transactionId &&
+ x.newTxState.major === TransactionMajorState.Done,
+ );
+
+ await wallet2.client.call(WalletApiOperation.ConfirmPeerPullDebit, {
+ transactionId: checkResp.transactionId,
+ });
+
+ await peerPullCreditDoneCond;
+ await peerPullDebitDoneCond;
+
+ const txn1 = await wallet1.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ const txn2 = await wallet2.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log(`txn1: ${j2s(txn1)}`);
+ console.log(`txn2: ${j2s(txn2)}`);
+}
+
+runPeerPullLargeTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-peer-push-large.ts b/packages/taler-harness/src/integrationtests/test-peer-push-large.ts
new file mode 100644
index 000000000..b7fbe9f6e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-peer-push-large.ts
@@ -0,0 +1,177 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ WalletNotification,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+import { CoinConfig } from "../harness/denomStructures.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+];
+
+/**
+ * Run a test for a multi-batch peer push payment.
+ */
+export async function runPeerPushLargeTest(t: GlobalTestState) {
+ const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t, coinConfigList);
+
+ let allW1Notifications: WalletNotification[] = [];
+ let allW2Notifications: WalletNotification[] = [];
+
+ const w1 = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ handleNotification(wn) {
+ allW1Notifications.push(wn);
+ },
+ });
+ const w2 = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ handleNotification(wn) {
+ allW2Notifications.push(wn);
+ },
+ });
+
+ // Withdraw digital cash into the wallet.
+
+ const withdrawRes = await withdrawViaBankV2(t, {
+ walletClient: w1.walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:300",
+ });
+
+ await withdrawRes.withdrawalFinishedCond;
+
+ const purse_expiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const checkResp0 = await w1.walletClient.call(
+ WalletApiOperation.CheckPeerPushDebit,
+ {
+ amount: "TESTKUDOS:200" as AmountString,
+ },
+ );
+
+ t.assertAmountEquals(checkResp0.amountEffective, "TESTKUDOS:200");
+
+ const resp = await w1.walletClient.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ partialContractTerms: {
+ summary: "Hello World 🥺",
+ amount: "TESTKUDOS:200" as AmountString,
+ purse_expiration,
+ },
+ },
+ );
+
+ console.log(resp);
+
+ const peerPushReadyCond = w1.walletClient.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Pending &&
+ x.newTxState.minor === TransactionMinorState.Ready &&
+ x.transactionId === resp.transactionId,
+ );
+
+ await peerPushReadyCond;
+
+ const txDetails = await w1.walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: resp.transactionId,
+ },
+ );
+ t.assertDeepEqual(txDetails.type, TransactionType.PeerPushDebit);
+ t.assertTrue(!!txDetails.talerUri);
+
+ const checkResp = await w2.walletClient.call(
+ WalletApiOperation.PreparePeerPushCredit,
+ {
+ talerUri: txDetails.talerUri,
+ },
+ );
+
+ console.log(checkResp);
+
+ const acceptResp = await w2.walletClient.call(
+ WalletApiOperation.ConfirmPeerPushCredit,
+ {
+ transactionId: checkResp.transactionId,
+ },
+ );
+
+ console.log(acceptResp);
+
+ await w2.walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ const txn1 = await w1.walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ const txn2 = await w2.walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log(`txn1: ${j2s(txn1)}`);
+ console.log(`txn2: ${j2s(txn2)}`);
+}
+
+runPeerPushLargeTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts
index 6b47951bc..ac118e4eb 100644
--- a/packages/taler-harness/src/integrationtests/test-revocation.ts
+++ b/packages/taler-harness/src/integrationtests/test-revocation.ts
@@ -20,15 +20,15 @@
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig } from "../harness/denomStructures.js";
import {
- GlobalTestState,
+ BankService,
ExchangeService,
+ GlobalTestState,
MerchantService,
WalletCli,
- setupDb,
- BankService,
+ WalletClient,
delayMs,
generateRandomPayto,
- WalletClient,
+ setupDb,
} from "../harness/harness.js";
import {
SimpleTestEnvironmentNg,
@@ -208,7 +208,7 @@ export async function runRevocationTest(t: GlobalTestState) {
console.log(coinDump);
const coinPubList = coinDump.coins.map((x) => x.coin_pub);
await walletClient.call(WalletApiOperation.ForceRefresh, {
- coinPubList,
+ refreshCoinSpecs: coinPubList.map((x) => ({ coinPub: x })),
});
await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts
new file mode 100644
index 000000000..69b721789
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts
@@ -0,0 +1,150 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+/**
+ * Run test for refreshe after a payment.
+ */
+export async function runWalletBlockedDepositTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+ ];
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t, coinConfigList);
+
+ // Withdraw digital cash into the wallet.
+
+ const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient: w1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Prevent the wallet from doing refreshes by injecting a 5xx
+ // status for all refresh requests.
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-block-refresh",
+ });
+
+ await makeTestPaymentV2(t, {
+ merchant,
+ walletClient: w1,
+ order: {
+ summary: "test",
+ amount: "TESTKUDOS:2",
+ },
+ });
+
+ const userPayto = generateRandomPayto("foo");
+
+ const bal = await w1.call(WalletApiOperation.GetBalances, {});
+ console.log(`balance: ${j2s(bal)}`);
+
+ const balDet = await w1.call(WalletApiOperation.GetBalanceDetail, {
+ currency: "TESTKUDOS",
+ });
+ console.log(`balance details: ${j2s(balDet)}`);
+
+ const depositCheckResp = await w1.call(WalletApiOperation.PrepareDeposit, {
+ amount: "TESTKUDOS:18" as AmountString,
+ depositPaytoUri: userPayto,
+ });
+
+ console.log(`check resp: ${j2s(depositCheckResp)}`);
+
+ const depositCreateResp = await w1.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:18" as AmountString,
+ depositPaytoUri: userPayto,
+ },
+ );
+
+ console.log(`create resp: ${j2s(depositCreateResp)}`);
+
+ const depositTrackCond = w1.waitForNotificationCond((n) => {
+ return (
+ n.type === NotificationType.TransactionStateTransition &&
+ n.transactionId === depositCreateResp.transactionId &&
+ n.newTxState.major === TransactionMajorState.Pending &&
+ n.newTxState.minor === TransactionMinorState.Track
+ );
+ });
+
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+ });
+
+ await depositTrackCond;
+}
+
+runWalletBlockedDepositTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
new file mode 100644
index 000000000..004de87c8
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
@@ -0,0 +1,142 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ MerchantApiClient,
+ PreparePayResultType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+];
+
+/**
+ * Run test for paying a merchant with balance locked behind a pending refresh.
+ */
+export async function runWalletBlockedPayMerchantTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ coinConfigList,
+ );
+
+ // Withdraw digital cash into the wallet.
+
+ const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient: w1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Prevent the wallet from doing refreshes by injecting a 5xx
+ // status for all refresh requests.
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-block-refresh",
+ });
+
+ // Do a payment that causes a refresh.
+ await makeTestPaymentV2(t, {
+ merchant,
+ walletClient: w1,
+ order: {
+ summary: "test",
+ amount: "TESTKUDOS:2",
+ },
+ });
+
+ const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
+
+ const orderResp = await merchantClient.createOrder({
+ order: {
+ summary: "My Payment",
+ amount: "TESTKUDOS:18",
+ },
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await w1.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
+
+ console.log(`prepare pay result: ${j2s(preparePayResult)}`);
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWalletBlockedPayMerchantTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts
new file mode 100644
index 000000000..36a6fea05
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts
@@ -0,0 +1,177 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+/**
+ * Run test for a peer push payment with balance locked behind a pending refresh.
+ */
+export async function runWalletBlockedPayPeerPullTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+ ];
+
+ const { bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ coinConfigList,
+ );
+
+ // Withdraw digital cash into the wallet.
+
+ const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ const { walletClient: w2 } = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient: w1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Prevent the wallet from doing refreshes by injecting a 5xx
+ // status for all refresh requests.
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-block-refresh",
+ });
+
+ // Do a payment that causes a refresh.
+ await makeTestPaymentV2(t, {
+ merchant,
+ walletClient: w1,
+ order: {
+ summary: "test",
+ amount: "TESTKUDOS:2",
+ },
+ });
+
+ await w2.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ const pullCreditReadyCond = w2.waitForNotificationCond((n) => {
+ return (
+ n.type === NotificationType.TransactionStateTransition &&
+ n.transactionId.startsWith("txn:peer-pull-credit:") &&
+ n.newTxState.major === TransactionMajorState.Pending &&
+ n.newTxState.minor === TransactionMinorState.Ready
+ );
+ });
+
+ const initResp = await w2.call(WalletApiOperation.InitiatePeerPullCredit, {
+ partialContractTerms: {
+ summary: "hi!",
+ amount: "TESTKUDOS:18" as AmountString,
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ },
+ });
+
+ await pullCreditReadyCond;
+
+ const initTx = await w2.call(WalletApiOperation.GetTransactionById, {
+ transactionId: initResp.transactionId,
+ });
+
+ t.assertDeepEqual(initTx.type, TransactionType.PeerPullCredit);
+ t.assertTrue(!!initTx.talerUri);
+
+ const checkResp = await w1.call(WalletApiOperation.PreparePeerPullDebit, {
+ talerUri: initTx.talerUri,
+ });
+
+ console.log(`check resp ${j2s(checkResp)}`);
+
+ const confirmResp = await w1.call(WalletApiOperation.ConfirmPeerPullDebit, {
+ transactionId: checkResp.transactionId,
+ });
+
+ console.log(`confirm resp ${j2s(confirmResp)}`);
+
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWalletBlockedPayPeerPullTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts
new file mode 100644
index 000000000..7427f2b07
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts
@@ -0,0 +1,149 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Duration,
+ NotificationType,
+ TransactionMajorState,
+ TransactionMinorState,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+ makeTestPaymentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+/**
+ * Run test for a peer push payment with balance locked behind a pending refresh.
+ */
+export async function runWalletBlockedPayPeerPushTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+ ];
+
+ const { bank, exchange, merchant } = await createSimpleTestkudosEnvironmentV2(
+ t,
+ coinConfigList,
+ );
+
+ // Withdraw digital cash into the wallet.
+
+ const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
+ name: "w1",
+ persistent: true,
+ config: {
+ testing: {
+ devModeActive: true,
+ },
+ },
+ });
+
+ await withdrawViaBankV2(t, {
+ walletClient: w1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ // Prevent the wallet from doing refreshes by injecting a 5xx
+ // status for all refresh requests.
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-block-refresh",
+ });
+
+ // Do a payment that causes a refresh.
+ await makeTestPaymentV2(t, {
+ merchant,
+ walletClient: w1,
+ order: {
+ summary: "test",
+ amount: "TESTKUDOS:2",
+ },
+ });
+
+ const checkResp = await w1.call(WalletApiOperation.CheckPeerPushDebit, {
+ amount: "TESTKUDOS:18" as AmountString,
+ });
+
+ console.log(`check resp ${j2s(checkResp)}`);
+
+ const readyCond = w1.waitForNotificationCond((n) => {
+ return (
+ n.type === NotificationType.TransactionStateTransition &&
+ n.transactionId.startsWith("txn:peer-push-debit:") &&
+ n.newTxState.major === TransactionMajorState.Pending &&
+ n.newTxState.minor === TransactionMinorState.Ready
+ );
+ });
+
+ const confirmResp = await w1.call(WalletApiOperation.InitiatePeerPushDebit, {
+ partialContractTerms: {
+ summary: "hi!",
+ amount: "TESTKUDOS:18" as AmountString,
+ purse_expiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ },
+ });
+
+ console.log(`confirm resp ${j2s(confirmResp)}`);
+
+ await w1.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+ });
+
+ await readyCond;
+}
+
+runWalletBlockedPayPeerPushTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-config.ts b/packages/taler-harness/src/integrationtests/test-wallet-config.ts
index 81a723473..461574031 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-config.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-config.ts
@@ -45,9 +45,11 @@ export async function runWalletConfigTest(t: GlobalTestState) {
exchanges: [
{
exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ currencyHint: "KUDOS",
},
{
exchangeBaseUrl: "https://exchange.test.taler.net/",
+ currencyHint: "TESTKUDOS",
},
],
},
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
index fadb34732..a089d99b5 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
@@ -35,7 +35,7 @@ import {
downloadExchangeInfo,
findDenomOrThrow,
refreshCoin,
- topupReserveWithDemobank,
+ topupReserveWithBank,
withdrawCoin,
} from "@gnu-taler/taler-wallet-core/dbless";
import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
@@ -71,7 +71,7 @@ export async function runWalletDblessTest(t: GlobalTestState) {
method: "GET",
});
- await topupReserveWithDemobank({
+ await topupReserveWithBank({
amount: "TESTKUDOS:10" as AmountString,
http,
reservePub: reserveKeyPair.pub,
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts b/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts
new file mode 100644
index 000000000..4ce8cde4c
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-denom-expire.ts
@@ -0,0 +1,154 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { Duration, Logger, NotificationType, j2s } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ generateRandomPayto,
+ setupDb,
+} from "../harness/harness.js";
+import {
+ applyTimeTravelV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const logger = new Logger("test-exchange-timetravel.ts");
+
+/**
+ * Test how the wallet handles an expired denomination.
+ */
+export async function runWalletDenomExpireTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ exchange.addCoinConfigList(makeNoFeeCoinConfig("TESTKUDOS"));
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ console.log("merchant started, configuring instances");
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ });
+
+ console.log("setup done!");
+
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "default",
+ });
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:15",
+ });
+ await wres.withdrawalFinishedCond;
+
+ const denomLossCond = walletClient.waitForNotificationCond((n) => {
+ return (
+ n.type === NotificationType.TransactionStateTransition &&
+ n.transactionId.startsWith("txn:denom-loss:")
+ );
+ });
+
+ // Travel into the future, the deposit expiration is two years
+ // into the future.
+ console.log("applying first time travel");
+ await applyTimeTravelV2(
+ Duration.toMilliseconds(Duration.fromSpec({ days: 800 })),
+ {
+ walletClient,
+ exchange,
+ merchant,
+ },
+ );
+
+ t.logStep("before-wait-denom-loss");
+
+ // Should be detected automatically, as exchange entry is surely outdated.
+ await denomLossCond;
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(`balances: ${j2s(bal)}`);
+
+ const txns = await walletClient.call(WalletApiOperation.GetTransactions, {
+ sort: "stable-ascending",
+ includeRefreshes: true,
+ });
+ console.log(`transactions: ${j2s(txns)}`);
+}
+
+runWalletDenomExpireTest.suites = ["exchange"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
new file mode 100644
index 000000000..3251750da
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts
@@ -0,0 +1,165 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ ExchangeUpdateStatus,
+ NotificationType,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ setupDb,
+} from "../harness/harness.js";
+import {
+ createWalletDaemonWithClient,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+/**
+ * Test how the wallet reacts when an exchange unexpectedly updates
+ * properties like the master public key.
+ */
+export async function runWalletExchangeUpdateTest(
+ t: GlobalTestState,
+): Promise<void> {
+ // Set up test environment
+
+ const db = await setupDb(t);
+ const db2 = await setupDb(t, {
+ nameSuffix: "two",
+ });
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchangeOne = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ // Danger: The second exchange has the same port!
+ // That's because we want it to have the same base URL,
+ // and we'll only start on of them at a time.
+ const exchangeTwo = ExchangeService.create(t, {
+ name: "testexchange-2",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db2.connStr,
+ });
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+
+ await exchangeOne.addBankAccount("1", exchangeBankAccount);
+ await exchangeTwo.addBankAccount("1", exchangeBankAccount);
+
+ // Same anyway.
+ bank.setSuggestedExchange(exchangeOne, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ exchangeOne.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS")));
+ exchangeTwo.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS")));
+
+ // Only start first exchange.
+ await exchangeOne.start();
+
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "wallet",
+ persistent: true,
+ });
+
+ // Since the default exchanges can change, we start the wallet in tests
+ // with no built-in defaults. Thus the list of exchanges is empty here.
+ const exchangesListResult = await walletClient.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+
+ t.assertDeepEqual(exchangesListResult.exchanges.length, 0);
+
+ const wres = await withdrawViaBankV2(t, {
+ walletClient,
+ bank,
+ exchange: exchangeOne,
+ amount: "TESTKUDOS:10",
+ });
+
+ await wres.withdrawalFinishedCond;
+
+ await exchangeOne.stop();
+
+ console.log("starting second exchange");
+ await exchangeTwo.start();
+
+ console.log("updating exchange entry");
+
+ await t.assertThrowsAsync(async () => {
+ await walletClient.call(WalletApiOperation.UpdateExchangeEntry, {
+ exchangeBaseUrl: exchangeOne.baseUrl,
+ force: true,
+ });
+ });
+
+ const exchangeEntry = await walletClient.call(
+ WalletApiOperation.GetExchangeEntryByUrl,
+ {
+ exchangeBaseUrl: exchangeOne.baseUrl,
+ },
+ );
+
+ console.log(`exchange entry: ${j2s(exchangeEntry)}`);
+
+ await t.assertThrowsAsync(async () => {
+ await walletClient.call(WalletApiOperation.GetWithdrawalDetailsForAmount, {
+ amount: "TESTKUDOS:10" as AmountString,
+ exchangeBaseUrl: exchangeOne.baseUrl,
+ });
+ });
+
+ const exchangeAvailableCond = walletClient.waitForNotificationCond((n) => {
+ console.log(`got notif ${j2s(n)}`);
+ return (
+ n.type === NotificationType.ExchangeStateTransition &&
+ n.newExchangeState.exchangeUpdateStatus === ExchangeUpdateStatus.Ready
+ );
+ });
+
+ await exchangeTwo.stop();
+
+ console.log("starting first exchange");
+ await exchangeOne.start();
+
+ await exchangeAvailableCond;
+}
+
+runWalletExchangeUpdateTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts b/packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts
new file mode 100644
index 000000000..0f1efd35e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-refresh-errors.ts
@@ -0,0 +1,107 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import { AmountString } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ feeWithdraw: "TESTKUDOS:0",
+ rsaKeySize: 1024,
+};
+
+/**
+ * Run test for refreshe after a payment.
+ */
+export async function runWalletRefreshErrorsTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfigList: CoinConfig[] = [
+ {
+ ...coinCommon,
+ name: "n1",
+ value: "TESTKUDOS:1",
+ },
+ {
+ ...coinCommon,
+ name: "n5",
+ value: "TESTKUDOS:5",
+ },
+ ];
+
+ const { walletClient, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t, coinConfigList);
+
+ const wres = await withdrawViaBankV2(t, {
+ amount: "TESTKUDOS:5",
+ bank,
+ exchange,
+ walletClient,
+ });
+ await wres.withdrawalFinishedCond;
+
+ const backupResp = await walletClient.call(
+ WalletApiOperation.CreateStoredBackup,
+ {},
+ );
+
+ const coinDump = await walletClient.call(WalletApiOperation.DumpCoins, {});
+
+ t.assertDeepEqual(coinDump.coins.length, 1);
+
+ await walletClient.call(WalletApiOperation.ForceRefresh, {
+ refreshCoinSpecs: [
+ {
+ coinPub: coinDump.coins[0].coin_pub,
+ amount: "TESTKUDOS:3" as AmountString,
+ },
+ ],
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ await walletClient.call(WalletApiOperation.RecoverStoredBackup, {
+ name: backupResp.name,
+ });
+
+ await walletClient.call(WalletApiOperation.ForceRefresh, {
+ refreshCoinSpecs: [
+ {
+ coinPub: coinDump.coins[0].coin_pub,
+ amount: "TESTKUDOS:3" as AmountString,
+ },
+ ],
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWalletRefreshErrorsTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts
new file mode 100644
index 000000000..23c0f938c
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-handover.ts
@@ -0,0 +1,191 @@
+/*
+ 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,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+ j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+} from "../harness/helpers.js";
+
+/**
+ * Test handing over a withdrawal to another wallet.
+ */
+export async function runWithdrawalHandoverTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bank, exchange } =
+ await createSimpleTestkudosEnvironmentV2(t);
+
+ // Do one normal withdrawal with the new split API
+ {
+ // Create a withdrawal operation
+
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
+ );
+ const user = await bankAccessApiClient.createRandomBankUser();
+ bankAccessApiClient.setAuth(user);
+ const wop = await bankAccessApiClient.createWithdrawalOperation(
+ user.username,
+ "TESTKUDOS:10",
+ );
+
+ const checkResp = await walletClient.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ t.assertTrue(!!checkResp.defaultExchangeBaseUrl);
+
+ const prepareResp = await walletClient.call(
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: checkResp.defaultExchangeBaseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ console.log(`prepareResp: ${j2s(prepareResp)}`);
+
+ const txns1 = await walletClient.call(WalletApiOperation.GetTransactions, {
+ sort: "stable-ascending",
+ });
+ console.log(j2s(txns1));
+
+ await walletClient.call(WalletApiOperation.ConfirmWithdrawal, {
+ transactionId: prepareResp.transactionId,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: prepareResp.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BankConfirmTransfer,
+ },
+ });
+
+ await bankAccessApiClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: prepareResp.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+ }
+
+ // Do one another withdrawal with handover.
+ {
+ t.logStep("start-subtest-handover");
+
+ const w2 = await createWalletDaemonWithClient(t, {
+ name: "w2",
+ });
+
+ // Create a withdrawal operation
+
+ const bankAccessApiClient = new TalerCorebankApiClient(
+ bank.corebankApiBaseUrl,
+ );
+ const user = await bankAccessApiClient.createRandomBankUser();
+ bankAccessApiClient.setAuth(user);
+ const wop = await bankAccessApiClient.createWithdrawalOperation(
+ user.username,
+ "TESTKUDOS:10",
+ );
+
+ const checkResp = await walletClient.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ t.assertTrue(!!checkResp.defaultExchangeBaseUrl);
+
+ const prepareRespW1 = await walletClient.call(
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: checkResp.defaultExchangeBaseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ const prepareRespW2 = await w2.walletClient.call(
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: checkResp.defaultExchangeBaseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ await w2.walletClient.call(WalletApiOperation.ConfirmWithdrawal, {
+ transactionId: prepareRespW2.transactionId,
+ });
+
+ await w2.walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: prepareRespW2.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BankConfirmTransfer,
+ },
+ });
+
+ await bankAccessApiClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ console.log(`wopid is ${wop.withdrawal_id}`);
+
+ t.logStep("start-wait-w2-done");
+ await w2.walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: prepareRespW2.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+ t.logStep("done-wait-w2-done");
+
+ t.logStep("start-wait-w1-done");
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: prepareRespW1.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Aborted,
+ minor: TransactionMinorState.CompletedByOtherWallet,
+ },
+ });
+
+ t.logStep("done-wait-w1-done");
+ }
+}
+
+runWithdrawalHandoverTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 566350770..4b23d7762 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -72,6 +72,8 @@ import { runPaymentTransientTest } from "./test-payment-transient.js";
import { runPaymentZeroTest } from "./test-payment-zero.js";
import { runPaymentTest } from "./test-payment.js";
import { runPaywallFlowTest } from "./test-paywall-flow.js";
+import { runPeerPullLargeTest } from "./test-peer-pull-large.js";
+import { runPeerPushLargeTest } from "./test-peer-push-large.js";
import { runPeerRepairTest } from "./test-peer-repair.js";
import { runPeerToPeerPullTest } from "./test-peer-to-peer-pull.js";
import { runPeerToPeerPushTest } from "./test-peer-to-peer-push.js";
@@ -90,16 +92,23 @@ import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend
import { runWalletBalanceNotificationsTest } from "./test-wallet-balance-notifications.js";
import { runWalletBalanceZeroTest } from "./test-wallet-balance-zero.js";
import { runWalletBalanceTest } from "./test-wallet-balance.js";
+import { runWalletBlockedDepositTest } from "./test-wallet-blocked-deposit.js";
+import { runWalletBlockedPayMerchantTest } from "./test-wallet-blocked-pay-merchant.js";
+import { runWalletBlockedPayPeerPullTest } from "./test-wallet-blocked-pay-peer-pull.js";
+import { runWalletBlockedPayPeerPushTest } from "./test-wallet-blocked-pay-peer-push.js";
import { runWalletCliTerminationTest } from "./test-wallet-cli-termination.js";
import { runWalletConfigTest } from "./test-wallet-config.js";
import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js";
import { runWalletDblessTest } from "./test-wallet-dbless.js";
import { runWalletDd48Test } from "./test-wallet-dd48.js";
+import { runWalletDenomExpireTest } from "./test-wallet-denom-expire.js";
import { runWalletDevExperimentsTest } from "./test-wallet-dev-experiments.js";
+import { runWalletExchangeUpdateTest } from "./test-wallet-exchange-update.js";
import { runWalletGenDbTest } from "./test-wallet-gendb.js";
import { runWalletInsufficientBalanceTest } from "./test-wallet-insufficient-balance.js";
import { runWalletNotificationsTest } from "./test-wallet-notifications.js";
import { runWalletObservabilityTest } from "./test-wallet-observability.js";
+import { runWalletRefreshErrorsTest } from "./test-wallet-refresh-errors.js";
import { runWalletRefreshTest } from "./test-wallet-refresh.js";
import { runWalletWirefeesTest } from "./test-wallet-wirefees.js";
import { runWallettestingTest } from "./test-wallettesting.js";
@@ -108,6 +117,7 @@ import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrat
import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js";
import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
+import { runWithdrawalHandoverTest } from "./test-withdrawal-handover.js";
import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js";
import { runWithdrawalManualTest } from "./test-withdrawal-manual.js";
@@ -210,6 +220,16 @@ const allTests: TestMainFunction[] = [
runWalletInsufficientBalanceTest,
runWalletWirefeesTest,
runDenomLostTest,
+ runWalletDenomExpireTest,
+ runWalletBlockedDepositTest,
+ runWalletBlockedPayMerchantTest,
+ runWalletBlockedPayPeerPushTest,
+ runWalletBlockedPayPeerPullTest,
+ runWalletExchangeUpdateTest,
+ runWalletRefreshErrorsTest,
+ runPeerPullLargeTest,
+ runPeerPushLargeTest,
+ runWithdrawalHandoverTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json
index dca73cecd..74b2d6155 100644
--- a/packages/taler-util/package.json
+++ b/packages/taler-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/taler-util",
- "version": "0.10.0",
+ "version": "0.10.7",
"description": "Generic helper functionality for GNU Taler",
"type": "module",
"types": "./lib/index.node.d.ts",
@@ -52,7 +52,11 @@
"default": "./lib/http-impl.missing.js"
},
"#argon2-impl": {
- "node": "./lib/argon2-impl.node.js",
+ "node": "./lib/argon2-impl.wasm.js",
+ "deno": "./lib/argon2-impl.wasm.js",
+ "worker": "./lib/argon2-impl.wasm.js",
+ "browser": "./lib/argon2-impl.wasm.js",
+ "webpack": "./lib/argon2-impl.wasm.js",
"default": "./lib/argon2-impl.missing.js"
}
},
diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts
index fa9a00176..c27f1d582 100644
--- a/packages/taler-util/src/MerchantApiClient.ts
+++ b/packages/taler-util/src/MerchantApiClient.ts
@@ -25,6 +25,7 @@ import {
createPlatformHttpLib,
expectSuccessResponseOrThrow,
readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
} from "./http.js";
import { FacadeCredentials } from "./libeufin-api-types.js";
import { LibtoolVersion } from "./libtool-version.js";
@@ -305,7 +306,7 @@ export class MerchantApiClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -321,7 +322,7 @@ export class MerchantApiClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -342,7 +343,7 @@ export class MerchantApiClient {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForMerchantConfig());
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -362,7 +363,7 @@ export class MerchantApiClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
diff --git a/packages/taler-util/src/argon2-impl.node.ts b/packages/taler-util/src/argon2-impl.wasm.ts
index d1a36c4fe..d1a36c4fe 100644
--- a/packages/taler-util/src/argon2-impl.node.ts
+++ b/packages/taler-util/src/argon2-impl.wasm.ts
diff --git a/packages/taler-util/src/bank-api-client.ts b/packages/taler-util/src/bank-api-client.ts
index 9c35af948..51359129d 100644
--- a/packages/taler-util/src/bank-api-client.ts
+++ b/packages/taler-util/src/bank-api-client.ts
@@ -45,6 +45,7 @@ import {
createPlatformHttpLib,
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
const logger = new Logger("bank-api-client.ts");
@@ -420,7 +421,7 @@ export class TalerCorebankApiClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts
index 701fc8835..54d450d82 100644
--- a/packages/taler-util/src/codec.ts
+++ b/packages/taler-util/src/codec.ts
@@ -361,6 +361,40 @@ export function codecForStringURL(shouldEndWithSlash?: boolean): Codec<string> {
}
/**
+ * Return a codec for a value that must be a string.
+ */
+export function codecForURL(shouldEndWithSlash?: boolean): Codec<URL> {
+ return {
+ decode(x: any, c?: Context): URL {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (shouldEndWithSlash && !x.endsWith("/")) {
+ throw new DecodingError(
+ `expected URL string that ends with slash at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ }
+ try {
+ const url = new URL(x);
+ return url;
+ } catch (e) {
+ if (e instanceof Error) {
+ throw new DecodingError(e.message);
+ } else {
+ throw new DecodingError(
+ `expected an URL string at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ }
+ },
+ };
+}
+
+/**
* Codec that allows any value.
*/
export function codecForAny(): Codec<any> {
@@ -457,6 +491,19 @@ export function codecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> {
};
}
+export function codecForLazy<V>(innerCodec: () => Codec<V>): Codec<V> {
+ let instance: Codec<V> | undefined = undefined
+ return {
+ decode(x: any, c?: Context): V {
+ if (instance === undefined) {
+ instance = innerCodec()
+ }
+ return instance.decode(x, c);
+ },
+ };
+}
+
+
export type CodecType<T> = T extends Codec<infer X> ? X : any;
export function codecForEither<T extends Array<Codec<unknown>>>(
@@ -480,5 +527,3 @@ export function codecForEither<T extends Array<Codec<unknown>>>(
},
};
}
-
-const x = codecForEither(codecForString(), codecForNumber());
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
index 11f01a3fe..9378d25e8 100644
--- a/packages/taler-util/src/errors.ts
+++ b/packages/taler-util/src/errors.ts
@@ -25,6 +25,7 @@
*/
import {
AbsoluteTime,
+ CancellationToken,
PaymentInsufficientBalanceDetails,
TalerErrorCode,
TalerErrorDetail,
@@ -277,6 +278,10 @@ export class TalerError<T = any> extends Error {
}
}
+export function safeStringifyException(e: any): string {
+ return JSON.stringify(getErrorDetailFromException(e), undefined, 2);
+}
+
/**
* Convert an exception (or anything that was thrown) into
* a TalerErrorDetail object.
@@ -285,6 +290,13 @@ export function getErrorDetailFromException(e: any): TalerErrorDetail {
if (e instanceof TalerError) {
return e.errorDetail;
}
+ if (e instanceof CancellationToken.CancellationError) {
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_CORE_REQUEST_CANCELLED,
+ {},
+ );
+ return err;
+ }
if (e instanceof Error) {
const err = makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
diff --git a/packages/taler-util/src/http-client/authentication.ts b/packages/taler-util/src/http-client/authentication.ts
index f77df2ed0..8897a2fa0 100644
--- a/packages/taler-util/src/http-client/authentication.ts
+++ b/packages/taler-util/src/http-client/authentication.ts
@@ -22,6 +22,7 @@ import {
HttpRequestLibrary,
createPlatformHttpLib,
makeBasicAuthHeader,
+ readTalerErrorResponse,
} from "../http.js";
import { LibtoolVersion } from "../libtool-version.js";
import {
@@ -82,7 +83,7 @@ export class TalerAuthenticationHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -91,14 +92,14 @@ export class TalerAuthenticationHttpClient {
* @returns
*/
async createAccessTokenBearer(
- token: string,
+ token: AccessToken,
body: TalerAuthentication.TokenRequest,
) {
const url = new URL(`token`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
headers: {
- Authorization: makeBearerTokenAuthHeader(token as AccessToken),
+ Authorization: makeBearerTokenAuthHeader(token),
},
body,
});
@@ -111,7 +112,7 @@ export class TalerAuthenticationHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -130,7 +131,7 @@ export class TalerAuthenticationHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
}
diff --git a/packages/taler-util/src/http-client/bank-conversion.ts b/packages/taler-util/src/http-client/bank-conversion.ts
index 3db9df101..cb14d8b34 100644
--- a/packages/taler-util/src/http-client/bank-conversion.ts
+++ b/packages/taler-util/src/http-client/bank-conversion.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import { AmountJson, Amounts } from "../amounts.js";
-import { HttpRequestLibrary } from "../http-common.js";
+import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
import { LibtoolVersion } from "../libtool-version.js";
@@ -94,7 +94,7 @@ export class TalerBankConversionHttpClient {
case HttpStatusCode.NotImplemented:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -138,7 +138,7 @@ export class TalerBankConversionHttpClient {
case HttpStatusCode.NotImplemented:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -185,7 +185,7 @@ export class TalerBankConversionHttpClient {
case HttpStatusCode.NotImplemented:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -217,7 +217,7 @@ export class TalerBankConversionHttpClient {
case HttpStatusCode.NotImplemented:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
}
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
index 7a98b6281..6c8051ada 100644
--- a/packages/taler-util/src/http-client/bank-core.ts
+++ b/packages/taler-util/src/http-client/bank-core.ts
@@ -15,12 +15,15 @@
*/
import {
+ AbsoluteTime,
HttpStatusCode,
LibtoolVersion,
LongPollParams,
+ OperationAlternative,
+ OperationFail,
+ OperationOk,
TalerErrorCode,
codecForChallenge,
- codecForTalerErrorDetail,
codecForTanTransmission,
opKnownAlternativeFailure,
opKnownHttpFailure,
@@ -29,6 +32,7 @@ import {
import {
HttpRequestLibrary,
createPlatformHttpLib,
+ readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import {
FailCasesByMethod,
@@ -62,6 +66,7 @@ import {
} from "./types.js";
import {
CacheEvictor,
+ IdempotencyRetry,
addLongPollingParam,
addPaginationParams,
makeBearerTokenAuthHeader,
@@ -126,8 +131,10 @@ export class TalerCoreBankHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForCoreBankConfig());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -144,9 +151,9 @@ export class TalerCoreBankHttpClient {
body: TalerCorebankApi.RegisterAccountRequest,
) {
const url = new URL(`accounts`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (auth) {
- headers.Authorization = makeBearerTokenAuthHeader(auth)
+ headers.Authorization = makeBearerTokenAuthHeader(auth);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -165,31 +172,32 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.Unauthorized:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict: {
- const body = await resp.json();
- const details = codecForTalerErrorDetail().decode(body);
+ const details = await readTalerErrorResponse(resp);
switch (details.code) {
case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_UNALLOWED_DEBIT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT:
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_MISSING_TAN_INFO:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
@@ -219,19 +227,18 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict: {
- const body = await resp.json();
- const details = codecForTalerErrorDetail().decode(body);
+ const details = await readTalerErrorResponse(resp);
switch (details.code) {
case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -267,25 +274,26 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict: {
- const body = await resp.json();
- const details = codecForTalerErrorDetail().decode(body);
+ const details = await readTalerErrorResponse(resp);
switch (details.code) {
case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT:
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_MISSING_TAN_INFO:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -321,19 +329,18 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.Unauthorized:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict: {
- const body = await resp.json();
- const details = codecForTalerErrorDetail().decode(body);
+ const details = await readTalerErrorResponse(resp);
switch (details.code) {
case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -361,7 +368,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opFixedSuccess({ public_accounts: [] });
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -393,7 +400,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.Unauthorized:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -417,7 +424,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -455,7 +462,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -482,7 +489,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.Unauthorized:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -493,9 +500,25 @@ export class TalerCoreBankHttpClient {
async createTransaction(
auth: UserAndToken,
body: TalerCorebankApi.CreateTransactionRequest,
+ idempotencyCheck: IdempotencyRetry | undefined,
cid?: string,
- ) {
+ ): Promise<
+ //manually definition all return types because of recursion
+ | OperationOk<TalerCorebankApi.CreateTransactionResponse>
+ | OperationAlternative<HttpStatusCode.Accepted, TalerCorebankApi.Challenge>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.BadRequest>
+ | OperationFail<HttpStatusCode.Unauthorized>
+ | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT>
+ | OperationFail<TalerErrorCode.BANK_ADMIN_CREDITOR>
+ | OperationFail<TalerErrorCode.BANK_SAME_ACCOUNT>
+ | OperationFail<TalerErrorCode.BANK_UNKNOWN_CREDITOR>
+ | OperationFail<TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED>
+ > {
const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl);
+ if (idempotencyCheck) {
+ body.request_uid = idempotencyCheck.uid;
+ }
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
headers: {
@@ -520,23 +543,28 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict: {
- const body = await resp.json();
- const details = codecForTalerErrorDetail().decode(body);
+ const details = await readTalerErrorResponse(resp);
switch (details.code) {
case TalerErrorCode.BANK_ADMIN_CREDITOR:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_SAME_ACCOUNT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_UNALLOWED_DEBIT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED:
+ if (!idempotencyCheck) {
+ return opKnownTalerFailure(details.code, details);
+ }
+ const nextRetry = idempotencyCheck.next();
+ return this.createTransaction(auth, body, nextRetry, cid);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -574,7 +602,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.Unauthorized:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -609,21 +637,20 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict: {
- const body = await resp.json();
- const details = codecForTalerErrorDetail().decode(body);
+ const details = await readTalerErrorResponse(resp);
switch (details.code) {
case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_UNALLOWED_DEBIT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -653,7 +680,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -687,7 +714,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -725,35 +752,35 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict: {
- const body = await resp.json();
- const details = codecForTalerErrorDetail().decode(body);
+ const details = await readTalerErrorResponse(resp);
switch (details.code) {
case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL:
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_BAD_CONVERSION:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_UNALLOWED_DEBIT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
case HttpStatusCode.BadGateway: {
- const body = await resp.json();
- const details = codecForTalerErrorDetail().decode(body);
+ const details = await readTalerErrorResponse(resp);
switch (details.code) {
case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
case HttpStatusCode.NotImplemented:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -780,7 +807,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotImplemented:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -805,7 +832,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotImplemented:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -830,7 +857,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotImplemented:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -861,17 +888,16 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.BadGateway: {
- const body = await resp.json();
- const details = codecForTalerErrorDetail().decode(body);
+ const details = await readTalerErrorResponse(resp);
switch (details.code) {
case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -903,21 +929,20 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict: {
- const body = await resp.json();
- const details = codecForTalerErrorDetail().decode(body);
+ const details = await readTalerErrorResponse(resp);
switch (details.code) {
case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
case HttpStatusCode.TooManyRequests:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -933,7 +958,7 @@ export class TalerCoreBankHttpClient {
auth: AccessToken,
params: {
timeframe?: TalerCorebankApi.MonitorTimeframeParam;
- which?: number;
+ date?: AbsoluteTime;
} = {},
) {
const url = new URL(`monitor`, this.baseUrl);
@@ -943,8 +968,11 @@ export class TalerCoreBankHttpClient {
TalerCorebankApi.MonitorTimeframeParam[params.timeframe],
);
}
- if (params.which) {
- url.searchParams.set("which", String(params.which));
+ if (params.date) {
+ const { t_s: seconds } = AbsoluteTime.toProtocolTimestamp(params.date);
+ if (seconds !== "never") {
+ url.searchParams.set("date_s", String(seconds));
+ }
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -960,7 +988,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.Unauthorized:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts
index f63fa4445..75e6a627a 100644
--- a/packages/taler-util/src/http-client/bank-integration.ts
+++ b/packages/taler-util/src/http-client/bank-integration.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpRequestLibrary } from "../http-common.js";
+import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
import { LibtoolVersion } from "../libtool-version.js";
@@ -79,7 +79,7 @@ export class TalerBankIntegrationHttpClient {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForIntegrationBankConfig());
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -110,7 +110,7 @@ export class TalerBankIntegrationHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -136,23 +136,23 @@ export class TalerBankIntegrationHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict: {
- const body = await resp.json();
+ const body = await readTalerErrorResponse(resp);
const details = codecForTalerErrorDetail().decode(body);
switch (details.code) {
case TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_UNKNOWN_ACCOUNT:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE:
- return opKnownTalerFailure(details.code, resp);
+ return opKnownTalerFailure(details.code, details);
default:
- return opUnknownFailure(resp, body);
+ return opUnknownFailure(resp, details);
}
}
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -173,7 +173,7 @@ export class TalerBankIntegrationHttpClient {
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
}
diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts
index 3b6b3c258..34afe7d86 100644
--- a/packages/taler-util/src/http-client/bank-revenue.ts
+++ b/packages/taler-util/src/http-client/bank-revenue.ts
@@ -14,9 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpRequestLibrary, makeBasicAuthHeader } from "../http-common.js";
+import {
+ HttpRequestLibrary,
+ makeBasicAuthHeader,
+ readTalerErrorResponse,
+} from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
import {
FailCasesByMethod,
ResultByMethod,
@@ -27,7 +32,8 @@ import {
import {
LongPollParams,
PaginationParams,
- codecForMerchantIncomingHistory,
+ codecForRevenueConfig,
+ codecForRevenueIncomingHistory,
} from "./types.js";
import { addLongPollingParam, addPaginationParams } from "./utils.js";
@@ -38,6 +44,10 @@ export type TalerBankRevenueErrorsByMethod<
prop extends keyof TalerRevenueHttpClient,
> = FailCasesByMethod<TalerRevenueHttpClient, prop>;
+type UsernameAndPassword = {
+ username: string;
+ password: string;
+};
/**
* The API is used by the merchant (or other parties) to query
* for incoming transactions to their account.
@@ -47,50 +57,66 @@ export class TalerRevenueHttpClient {
constructor(
readonly baseUrl: string,
- readonly username: string,
httpClient?: HttpRequestLibrary,
) {
this.httpLib = httpClient ?? createPlatformHttpLib();
}
- // public readonly PROTOCOL_VERSION = "4:0:0";
- // isCompatible(version: string): boolean {
- // const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version)
- // return compare?.compatible ?? false
- // }
- // /**
- // * https://docs.taler.net/core/api-corebank.html#config
- // *
- // */
- // async getConfig() {
- // const url = new URL(`config`, this.baseUrl);
- // const resp = await this.httpLib.fetch(url.href, {
- // method: "GET"
- // });
- // switch (resp.status) {
- // case HttpStatusCode.Ok: return opSuccess(resp, codecForCoreBankConfig())
- // default: return opUnknownFailure(resp, await resp.text())
- // }
- // }
+ public readonly PROTOCOL_VERSION = "0:0:0";
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
/**
+ * https://docs.taler.net/core/api-bank-revenue.html#get--config
+ *
+ */
+ async getConfig(auth?: UsernameAndPassword) {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: auth
+ ? makeBasicAuthHeader(auth.username, auth.password)
+ : undefined,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForRevenueConfig());
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
* https://docs.taler.net/core/api-bank-revenue.html#get--history
*
* @returns
*/
- async getHistory(auth: string, params?: PaginationParams & LongPollParams) {
+ async getHistory(
+ auth?: UsernameAndPassword,
+ params?: PaginationParams & LongPollParams,
+ ) {
const url = new URL(`history`, this.baseUrl);
addPaginationParams(url, params);
addLongPollingParam(url, params);
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
headers: {
- Authorization: makeBasicAuthHeader(this.username, auth),
+ Authorization: auth
+ ? makeBasicAuthHeader(auth.username, auth.password)
+ : undefined,
},
});
switch (resp.status) {
case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForMerchantIncomingHistory());
+ return opSuccessFromHttp(resp, codecForRevenueIncomingHistory());
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Unauthorized:
@@ -98,7 +124,7 @@ export class TalerRevenueHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
}
diff --git a/packages/taler-util/src/http-client/bank-wire.ts b/packages/taler-util/src/http-client/bank-wire.ts
index 54211fef7..a8c976a80 100644
--- a/packages/taler-util/src/http-client/bank-wire.ts
+++ b/packages/taler-util/src/http-client/bank-wire.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpRequestLibrary, makeBasicAuthHeader } from "../http-common.js";
+import { HttpRequestLibrary, makeBasicAuthHeader, readTalerErrorResponse } from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
import {
@@ -77,7 +77,7 @@ export class TalerWireGatewayHttpClient {
// });
// switch (resp.status) {
// case HttpStatusCode.Ok: return opSuccess(resp, codecForCoreBankConfig())
- // default: return opUnknownFailure(resp, await resp.text())
+ // default: return opUnknownFailure(resp, await readTalerErrorResponse(resp))
// }
// }
@@ -108,7 +108,7 @@ export class TalerWireGatewayHttpClient {
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -147,7 +147,7 @@ export class TalerWireGatewayHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -186,7 +186,7 @@ export class TalerWireGatewayHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -220,7 +220,7 @@ export class TalerWireGatewayHttpClient {
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
}
diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts
new file mode 100644
index 000000000..aa530570d
--- /dev/null
+++ b/packages/taler-util/src/http-client/challenger.ts
@@ -0,0 +1,291 @@
+import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import { TalerCoreBankCacheEviction } from "../index.node.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import {
+ FailCasesByMethod,
+ RedirectResult,
+ ResultByMethod,
+ opFixedSuccess,
+ opKnownAlternativeFailure,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ AccessToken,
+ codecForChallengeCreateResponse,
+ codecForChallengeSetupResponse,
+ codecForChallengeStatus,
+ codecForChallengerAuthResponse,
+ codecForChallengerInfoResponse,
+ codecForChallengerTermsOfServiceResponse,
+ codecForInvalidPinResponse,
+} from "./types.js";
+import { CacheEvictor, makeBearerTokenAuthHeader, nullEvictor } from "./utils.js";
+
+export type ChallengerResultByMethod<prop extends keyof ChallengerHttpClient> =
+ ResultByMethod<ChallengerHttpClient, prop>;
+export type ChallengerErrorsByMethod<prop extends keyof ChallengerHttpClient> =
+ FailCasesByMethod<ChallengerHttpClient, prop>;
+
+export enum ChallengerCacheEviction {
+ CREATE_CHALLENGE,
+}
+
+/**
+ */
+export class ChallengerHttpClient {
+ httpLib: HttpRequestLibrary;
+ cacheEvictor: CacheEvictor<ChallengerCacheEviction>;
+ public readonly PROTOCOL_VERSION = "1:0:0";
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<ChallengerCacheEviction>,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+ /**
+ * https://docs.taler.net/core/api-challenger.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForChallengerTermsOfServiceResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--setup-$CLIENT_ID
+ *
+ */
+ async setup(clientId: string, token: AccessToken) {
+ const url = new URL(`setup/${clientId}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengeSetupResponse());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // LOGIN
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--authorize-$NONCE
+ *
+ */
+ async login(
+ nonce: string,
+ clientId: string,
+ redirectUri: string,
+ state: string | undefined,
+ ) {
+ const url = new URL(`authorize/${nonce}`, this.baseUrl);
+ url.searchParams.set("response_type", "code");
+ url.searchParams.set("client_id", clientId);
+ url.searchParams.set("redirect_uri", redirectUri);
+ if (state) {
+ url.searchParams.set("state", state);
+ }
+ // url.searchParams.set("scope", "code");
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengeStatus());
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // CHALLENGE
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--challenge-$NONCE
+ *
+ */
+ async challenge(nonce: string, body: Record<"email", string>) {
+ const url = new URL(`challenge/${nonce}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: new URLSearchParams(Object.entries(body)).toString(),
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ redirect: "manual",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ await this.cacheEvictor.notifySuccess(
+ ChallengerCacheEviction.CREATE_CHALLENGE,
+ );
+ return opSuccessFromHttp(resp, codecForChallengeCreateResponse());
+ }
+ case HttpStatusCode.Found:
+ const redirect = resp.headers.get("Location")!;
+ return opFixedSuccess<RedirectResult>({
+ redirectURL: new URL(redirect),
+ });
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.TooManyRequests:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // SOLVE
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--solve-$NONCE
+ *
+ */
+ async solve(nonce: string, body: Record<string, string>) {
+ const url = new URL(`solve/${nonce}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: new URLSearchParams(Object.entries(body)).toString(),
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ redirect: "manual",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Found:
+ const redirect = resp.headers.get("Location")!;
+ return opFixedSuccess<RedirectResult>({
+ redirectURL: new URL(redirect),
+ });
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForInvalidPinResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.TooManyRequests:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // AUTH
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--token
+ *
+ */
+ async token(
+ client_id: string,
+ redirect_uri: string,
+ client_secret: AccessToken,
+ code: string,
+ ) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: new URLSearchParams(
+ Object.entries({
+ client_id,
+ redirect_uri,
+ client_secret,
+ code,
+ grant_type: "authorization_code",
+ }),
+ ).toString(),
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengerAuthResponse());
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // INFO
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#get--info
+ *
+ */
+ async info(token: AccessToken) {
+ const url = new URL(`info`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengerInfoResponse());
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts
index c61ba1f8d..68d68267f 100644
--- a/packages/taler-util/src/http-client/exchange.ts
+++ b/packages/taler-util/src/http-client/exchange.ts
@@ -1,4 +1,4 @@
-import { HttpRequestLibrary } from "../http-common.js";
+import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
import { HttpStatusCode } from "../http-status-codes.js";
import { createPlatformHttpLib } from "../http.js";
import { LibtoolVersion } from "../libtool-version.js";
@@ -33,7 +33,7 @@ import {
codecForExchangeConfig,
codecForExchangeKeys,
} from "./types.js";
-import { addPaginationParams } from "./utils.js";
+import { CacheEvictor, addPaginationParams, nullEvictor } from "./utils.js";
export type TalerExchangeResultByMethod<
prop extends keyof TalerExchangeHttpClient,
@@ -42,17 +42,25 @@ export type TalerExchangeErrorsByMethod<
prop extends keyof TalerExchangeHttpClient,
> = FailCasesByMethod<TalerExchangeHttpClient, prop>;
+export enum TalerExchangeCacheEviction {
+ CREATE_DESCISION,
+}
+
+
/**
*/
export class TalerExchangeHttpClient {
httpLib: HttpRequestLibrary;
public readonly PROTOCOL_VERSION = "18:0:1";
+ cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>;
constructor(
readonly baseUrl: string,
httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<TalerExchangeCacheEviction>,
) {
this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
}
isCompatible(version: string): boolean {
@@ -60,6 +68,27 @@ export class TalerExchangeHttpClient {
return compare?.compatible ?? false;
}
/**
+ * https://docs.taler.net/core/api-exchange.html#get--seed
+ *
+ */
+ async getSeed() {
+ const url = new URL(`seed`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ const buffer = await resp.bytes();
+ const uintar = new Uint8Array(buffer);
+
+ return opFixedSuccess(uintar);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
* https://docs.taler.net/core/api-exchange.html#get--config
*
*/
@@ -71,8 +100,10 @@ export class TalerExchangeHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForExchangeConfig());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
@@ -89,7 +120,7 @@ export class TalerExchangeHttpClient {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForExchangeKeys());
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -136,7 +167,7 @@ export class TalerExchangeHttpClient {
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -169,7 +200,7 @@ export class TalerExchangeHttpClient {
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -203,7 +234,7 @@ export class TalerExchangeHttpClient {
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
}
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
index fec1e7143..d682dcfa0 100644
--- a/packages/taler-util/src/http-client/merchant.ts
+++ b/packages/taler-util/src/http-client/merchant.ts
@@ -16,9 +16,11 @@
import {
AccessToken,
+ FailCasesByMethod,
HttpStatusCode,
LibtoolVersion,
PaginationParams,
+ ResultByMethod,
TalerMerchantApi,
codecForAbortResponse,
codecForAccountAddResponse,
@@ -60,6 +62,7 @@ import {
HttpRequestLibrary,
HttpResponse,
createPlatformHttpLib,
+ readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import { opSuccessFromHttp, opUnknownFailure } from "../operation.js";
import {
@@ -69,11 +72,45 @@ import {
nullEvictor,
} from "./utils.js";
+export type TalerMerchantInstanceResultByMethod<
+ prop extends keyof TalerMerchantInstanceHttpClient,
+> = ResultByMethod<TalerMerchantInstanceHttpClient, prop>;
+export type TalerMerchantInstanceErrorsByMethod<
+ prop extends keyof TalerMerchantInstanceHttpClient,
+> = FailCasesByMethod<TalerMerchantInstanceHttpClient, prop>;
+
export enum TalerMerchantInstanceCacheEviction {
CREATE_ORDER,
+ UPDATE_ORDER,
+ DELETE_ORDER,
+ UPDATE_CURRENT_INSTANCE,
+ DELETE_CURRENT_INSTANCE,
+ CREATE_BANK_ACCOUNT,
+ UPDATE_BANK_ACCOUNT,
+ DELETE_BANK_ACCOUNT,
+ CREATE_PRODUCT,
+ UPDATE_PRODUCT,
+ DELETE_PRODUCT,
+ CREATE_TRANSFER,
+ DELETE_TRANSFER,
+ CREATE_DEVICE,
+ UPDATE_DEVICE,
+ DELETE_DEVICE,
+ CREATE_TEMPLATE,
+ UPDATE_TEMPLATE,
+ DELETE_TEMPLATE,
+ CREATE_WEBHOOK,
+ UPDATE_WEBHOOK,
+ DELETE_WEBHOOK,
+ CREATE_TOKENFAMILY,
+ UPDATE_TOKENFAMILY,
+ DELETE_TOKENFAMILY,
+ LAST,
}
export enum TalerMerchantManagementCacheEviction {
- CREATE_INSTANCE,
+ CREATE_INSTANCE = TalerMerchantInstanceCacheEviction.LAST + 1,
+ UPDATE_INSTANCE,
+ DELETE_INSTANCE,
}
/**
* Protocol version spoken with the core bank.
@@ -117,8 +154,10 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForMerchantConfig());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -138,14 +177,18 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.Ok:
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
return opSuccessFromHttp(resp, codecForClaimResponse());
+ }
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -161,8 +204,12 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.Ok:
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
return opSuccessFromHttp(resp, codecForPaymentResponse());
+ }
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.PaymentRequired:
@@ -184,7 +231,7 @@ export class TalerMerchantInstanceHttpClient {
case HttpStatusCode.GatewayTimeout:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -246,7 +293,7 @@ export class TalerMerchantInstanceHttpClient {
case HttpStatusCode.NotAcceptable:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -262,8 +309,12 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.Ok:
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
return opSuccessFromHttp(resp, codecForPaidRefundStatusResponse());
+ }
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Forbidden:
@@ -271,7 +322,7 @@ export class TalerMerchantInstanceHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -290,8 +341,12 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.Ok:
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
return opSuccessFromHttp(resp, codecForAbortResponse());
+ }
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Forbidden:
@@ -299,7 +354,7 @@ export class TalerMerchantInstanceHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -318,8 +373,12 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.Ok:
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
return opSuccessFromHttp(resp, codecForWalletRefundResponse());
+ }
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Forbidden:
@@ -327,7 +386,7 @@ export class TalerMerchantInstanceHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -344,9 +403,9 @@ export class TalerMerchantInstanceHttpClient {
) {
const url = new URL(`private/auth`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -355,14 +414,16 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.Ok:
+ case HttpStatusCode.Ok: // FIXME: missing in docs
return opEmptySuccess(resp);
- case HttpStatusCode.NoContent: // FIXME: missing in docs
+ case HttpStatusCode.NoContent:
return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -375,9 +436,9 @@ export class TalerMerchantInstanceHttpClient {
) {
const url = new URL(`private`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
@@ -385,12 +446,18 @@ export class TalerMerchantInstanceHttpClient {
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_CURRENT_INSTANCE,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -398,12 +465,12 @@ export class TalerMerchantInstanceHttpClient {
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private
*
*/
- async getCurrentInstance(token: AccessToken) {
+ async getCurrentInstanceDetails(token: AccessToken) {
const url = new URL(`private`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -413,24 +480,31 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForQueryInstancesResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private
*/
- async deleteCurrentInstance(token: AccessToken | undefined, params: { purge?: boolean } = {}) {
+ async deleteCurrentInstance(
+ token: AccessToken | undefined,
+ params: { purge?: boolean } = {},
+ ) {
const url = new URL(`private`, this.baseUrl);
- if (params.purge) {
- url.searchParams.set("purge", "YES");
+ if (params.purge !== undefined) {
+ url.searchParams.set("purge", params.purge ? "YES" : "NO");
}
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
@@ -438,8 +512,12 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_CURRENT_INSTANCE,
+ );
return opEmptySuccess(resp);
+ }
case HttpStatusCode.Unauthorized:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
@@ -447,7 +525,7 @@ export class TalerMerchantInstanceHttpClient {
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -456,7 +534,7 @@ export class TalerMerchantInstanceHttpClient {
*/
async getCurrentIntanceKycStatus(
token: AccessToken | undefined,
- params: TalerMerchantApi.GetKycStatusRequestParams,
+ params: TalerMerchantApi.GetKycStatusRequestParams = {},
) {
const url = new URL(`private/kyc`, this.baseUrl);
@@ -470,9 +548,9 @@ export class TalerMerchantInstanceHttpClient {
url.searchParams.set("timeout_ms", String(params.timeout));
}
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -484,14 +562,22 @@ export class TalerMerchantInstanceHttpClient {
return opSuccessFromHttp(resp, codecForAccountKycRedirects());
case HttpStatusCode.NoContent:
return opEmptySuccess(resp);
- case HttpStatusCode.BadGateway:
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForAccountKycRedirects(),
+ );
case HttpStatusCode.ServiceUnavailable:
return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Conflict:
+ case HttpStatusCode.GatewayTimeout:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -502,12 +588,15 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-accounts
*/
- async addAccount(token: AccessToken | undefined, body: TalerMerchantApi.AccountAddDetails) {
+ async addBankAccount(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.AccountAddDetails,
+ ) {
const url = new URL(`private/accounts`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -516,30 +605,36 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.Ok:
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_BANK_ACCOUNT,
+ );
return opSuccessFromHttp(resp, codecForAccountAddResponse());
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-accounts-$H_WIRE
*/
- async updateAccount(
+ async updateBankAccount(
token: AccessToken | undefined,
wireAccount: string,
body: TalerMerchantApi.AccountPatchDetails,
) {
const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
@@ -547,24 +642,32 @@ export class TalerMerchantInstanceHttpClient {
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_BANK_ACCOUNT,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts
*/
- async listAccounts(token: AccessToken) {
+ async listBankAccounts(token: AccessToken, params?: PaginationParams) {
const url = new URL(`private/accounts`, this.baseUrl);
- const headers: Record<string, string> = {}
+ // addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -574,22 +677,27 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForAccountsSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts-$H_WIRE
*/
- async getAccount(token: AccessToken | undefined, wireAccount: string) {
+ async getBankAccountDetails(
+ token: AccessToken | undefined,
+ wireAccount: string,
+ ) {
const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -599,22 +707,24 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForBankAccountEntry());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-accounts-$H_WIRE
*/
- async deleteAccount(token: AccessToken | undefined, wireAccount: string) {
+ async deleteBankAccount(token: AccessToken | undefined, wireAccount: string) {
const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
@@ -622,12 +732,18 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_BANK_ACCOUNT,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -638,12 +754,15 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-products
*/
- async addProduct(token: AccessToken | undefined, body: TalerMerchantApi.ProductAddDetail) {
+ async addProduct(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.ProductAddDetail,
+ ) {
const url = new URL(`private/products`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -652,12 +771,20 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_PRODUCT,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -667,13 +794,13 @@ export class TalerMerchantInstanceHttpClient {
async updateProduct(
token: AccessToken | undefined,
productId: string,
- body: TalerMerchantApi.ProductAddDetail,
+ body: TalerMerchantApi.ProductPatchDetail,
) {
const url = new URL(`private/products/${productId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
@@ -682,28 +809,37 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products
*/
- async listProducts(token: AccessToken | undefined, params?: PaginationParams) {
+ async listProducts(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
const url = new URL(`private/products`, this.baseUrl);
addMerchantPaginationParams(url, params);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -713,22 +849,24 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForInventorySummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: not in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products-$PRODUCT_ID
*/
- async getProduct(token: AccessToken | undefined, productId: string) {
+ async getProductDetails(token: AccessToken | undefined, productId: string) {
const url = new URL(`private/products/${productId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -738,22 +876,28 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForProductDetail());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#reserving-inventory
*/
- async lockProduct(token: AccessToken | undefined, productId: string, body: TalerMerchantApi.LockRequest) {
+ async lockProduct(
+ token: AccessToken | undefined,
+ productId: string,
+ body: TalerMerchantApi.LockRequest,
+ ) {
const url = new URL(`private/products/${productId}/lock`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -762,26 +906,32 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Gone:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#removing-products-from-inventory
*/
- async removeProduct(token: AccessToken | undefined, productId: string) {
+ async deleteProduct(token: AccessToken | undefined, productId: string) {
const url = new URL(`private/products/${productId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
@@ -789,14 +939,20 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_PRODUCT,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -807,29 +963,36 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders
*/
- async createOrder(token: AccessToken | undefined, body: TalerMerchantApi.PostOrderRequest) {
+ async createOrder(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.PostOrderRequest,
+ ) {
const url = new URL(`private/orders`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
body,
headers,
});
- return this.procesOrderCreationResponse(resp)
+ return this.procesOrderCreationResponse(resp);
}
private async procesOrderCreationResponse(resp: HttpResponse) {
switch (resp.status) {
case HttpStatusCode.Ok: {
- this.cacheEvictor.notifySuccess(TalerMerchantInstanceCacheEviction.CREATE_ORDER)
- return opSuccessFromHttp(resp, codecForPostOrderResponse())
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForPostOrderResponse());
}
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Gone:
@@ -839,14 +1002,17 @@ export class TalerMerchantInstanceHttpClient {
codecForOutOfStockResponse(),
);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#inspecting-orders
*/
- async listOrders(token: AccessToken | undefined, params: TalerMerchantApi.ListOrdersRequestParams = {}) {
+ async listOrders(
+ token: AccessToken | undefined,
+ params: TalerMerchantApi.ListOrdersRequestParams = {},
+ ) {
const url = new URL(`private/orders`, this.baseUrl);
if (params.date) {
@@ -855,11 +1021,11 @@ export class TalerMerchantInstanceHttpClient {
if (params.fulfillmentUrl) {
url.searchParams.set("fulfillment_url", params.fulfillmentUrl);
}
- if (params.paid) {
- url.searchParams.set("paid", "YES");
+ if (params.paid !== undefined) {
+ url.searchParams.set("paid", params.paid ? "YES" : "NO");
}
- if (params.refunded) {
- url.searchParams.set("refunded", "YES");
+ if (params.refunded !== undefined) {
+ url.searchParams.set("refunded", params.refunded ? "YES" : "NO");
}
if (params.sessionId) {
url.searchParams.set("session_id", params.sessionId);
@@ -867,14 +1033,14 @@ export class TalerMerchantInstanceHttpClient {
if (params.timeout) {
url.searchParams.set("timeout", String(params.timeout));
}
- if (params.wired) {
- url.searchParams.set("wired", "YES");
+ if (params.wired !== undefined) {
+ url.searchParams.set("wired", params.wired ? "YES" : "NO");
}
addMerchantPaginationParams(url, params);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -884,23 +1050,30 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForOrderHistory());
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-orders-$ORDER_ID
*/
- async getOrder(
+ async getOrderDetails(
token: AccessToken | undefined,
orderId: string,
params: TalerMerchantApi.GetOrderRequestParams = {},
) {
const url = new URL(`private/orders/${orderId}`, this.baseUrl);
- if (params.allowRefundedForRepurchase) {
- url.searchParams.set("allow_refunded_for_repurchase", "YES");
+ if (params.allowRefundedForRepurchase !== undefined) {
+ url.searchParams.set(
+ "allow_refunded_for_repurchase",
+ params.allowRefundedForRepurchase ? "YES" : "NO",
+ );
}
if (params.sessionId) {
url.searchParams.set("session_id", params.sessionId);
@@ -909,9 +1082,9 @@ export class TalerMerchantInstanceHttpClient {
url.searchParams.set("timeout_ms", String(params.timeout));
}
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -926,6 +1099,8 @@ export class TalerMerchantInstanceHttpClient {
);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.BadGateway:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.GatewayTimeout:
@@ -935,19 +1110,23 @@ export class TalerMerchantInstanceHttpClient {
codecForOutOfStockResponse(),
);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#private-order-data-cleanup
*/
- async forgetOrder(token: AccessToken | undefined, orderId: string, body: TalerMerchantApi.ForgetRequest) {
+ async forgetOrder(
+ token: AccessToken | undefined,
+ orderId: string,
+ body: TalerMerchantApi.ForgetRequest,
+ ) {
const url = new URL(`private/orders/${orderId}/forget`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
@@ -956,10 +1135,16 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.Ok:
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
return opEmptySuccess(resp);
+ }
case HttpStatusCode.NoContent:
return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.BadRequest:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
@@ -967,7 +1152,7 @@ export class TalerMerchantInstanceHttpClient {
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -977,9 +1162,9 @@ export class TalerMerchantInstanceHttpClient {
async deleteOrder(token: AccessToken | undefined, orderId: string) {
const url = new URL(`private/orders/${orderId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
@@ -987,14 +1172,20 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_ORDER,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1005,12 +1196,16 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders-$ORDER_ID-refund
*/
- async addRefund(token: AccessToken | undefined, orderId: string, body: TalerMerchantApi.RefundRequest) {
+ async addRefund(
+ token: AccessToken | undefined,
+ orderId: string,
+ body: TalerMerchantApi.RefundRequest,
+ ) {
const url = new URL(`private/orders/${orderId}/refund`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -1019,10 +1214,16 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.Ok:
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
return opSuccessFromHttp(resp, codecForMerchantRefundResponse());
+ }
case HttpStatusCode.Forbidden:
return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Gone:
@@ -1030,7 +1231,7 @@ export class TalerMerchantInstanceHttpClient {
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1041,12 +1242,15 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-transfers
*/
- async informWireTransfer(token: AccessToken | undefined, body: TalerMerchantApi.TransferInformation) {
+ async informWireTransfer(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.TransferInformation,
+ ) {
const url = new URL(`private/transfers`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -1055,14 +1259,20 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_TRANSFER,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1084,14 +1294,14 @@ export class TalerMerchantInstanceHttpClient {
if (params.paytoURI) {
url.searchParams.set("payto_uri", params.paytoURI);
}
- if (params.verified) {
- url.searchParams.set("verified", "YES");
+ if (params.verified !== undefined) {
+ url.searchParams.set("verified", params.verified ? "YES" : "NO");
}
addMerchantPaginationParams(url, params);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1101,8 +1311,12 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForTansferList());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1112,9 +1326,9 @@ export class TalerMerchantInstanceHttpClient {
async deleteWireTransfer(token: AccessToken | undefined, transferId: string) {
const url = new URL(`private/transfers/${transferId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
@@ -1122,14 +1336,20 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_TRANSFER,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1140,12 +1360,15 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-otp-devices
*/
- async addOtpDevice(token: AccessToken | undefined, body: TalerMerchantApi.OtpDeviceAddDetails) {
+ async addOtpDevice(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.OtpDeviceAddDetails,
+ ) {
const url = new URL(`private/otp-devices`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -1154,12 +1377,18 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_DEVICE,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1173,9 +1402,9 @@ export class TalerMerchantInstanceHttpClient {
) {
const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
@@ -1183,26 +1412,37 @@ export class TalerMerchantInstanceHttpClient {
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_DEVICE,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices
*/
- async listOtpDevices(token: AccessToken | undefined,) {
+ async listOtpDevices(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
const url = new URL(`private/otp-devices`, this.baseUrl);
- const headers: Record<string, string> = {}
+ addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1211,17 +1451,19 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForOtpDeviceSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID
*/
- async getOtpDevice(
+ async getOtpDeviceDetails(
token: AccessToken | undefined,
deviceId: string,
params: TalerMerchantApi.GetOtpDeviceRequestParams = {},
@@ -1234,9 +1476,9 @@ export class TalerMerchantInstanceHttpClient {
if (params.price) {
url.searchParams.set("price", params.price);
}
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1246,10 +1488,12 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForOtpDeviceDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1259,21 +1503,27 @@ export class TalerMerchantInstanceHttpClient {
async deleteOtpDevice(token: AccessToken | undefined, deviceId: string) {
const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_DEVICE,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1284,12 +1534,15 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-templates
*/
- async addTemplate(token: AccessToken | undefined, body: TalerMerchantApi.TemplateAddDetails) {
+ async addTemplate(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.TemplateAddDetails,
+ ) {
const url = new URL(`private/templates`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -1297,12 +1550,18 @@ export class TalerMerchantInstanceHttpClient {
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_TEMPLATE,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1316,9 +1575,9 @@ export class TalerMerchantInstanceHttpClient {
) {
const url = new URL(`private/templates/${templateId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
@@ -1326,26 +1585,35 @@ export class TalerMerchantInstanceHttpClient {
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_TEMPLATE,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#inspecting-template
*/
- async listTemplates(token: AccessToken | undefined,) {
+ async listTemplates(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
const url = new URL(`private/templates`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1354,22 +1622,24 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForTemplateSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID
*/
- async getTemplate(token: AccessToken | undefined, templateId: string) {
+ async getTemplateDetails(token: AccessToken | undefined, templateId: string) {
const url = new URL(`private/templates/${templateId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1378,10 +1648,12 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForTemplateDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1391,21 +1663,27 @@ export class TalerMerchantInstanceHttpClient {
async deleteTemplate(token: AccessToken | undefined, templateId: string) {
const url = new URL(`private/templates/${templateId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_TEMPLATE,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1424,7 +1702,7 @@ export class TalerMerchantInstanceHttpClient {
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1442,7 +1720,7 @@ export class TalerMerchantInstanceHttpClient {
body,
});
- return this.procesOrderCreationResponse(resp)
+ return this.procesOrderCreationResponse(resp);
}
//
@@ -1452,12 +1730,15 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-webhooks
*/
- async addWebhook(token: AccessToken | undefined, body: TalerMerchantApi.WebhookAddDetails) {
+ async addWebhook(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.WebhookAddDetails,
+ ) {
const url = new URL(`private/webhooks`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -1466,12 +1747,18 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_WEBHOOK,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1485,9 +1772,9 @@ export class TalerMerchantInstanceHttpClient {
) {
const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
@@ -1496,26 +1783,35 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_WEBHOOK,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks
*/
- async listWebhooks(token: AccessToken | undefined,) {
+ async listWebhooks(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
const url = new URL(`private/webhooks`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1523,24 +1819,26 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForWebhookSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
*/
- async getWebhook(token: AccessToken | undefined, webhookId: string) {
+ async getWebhookDetails(token: AccessToken | undefined, webhookId: string) {
const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1549,34 +1847,42 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.NoContent:
return opSuccessFromHttp(resp, codecForWebhookDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
*/
- async removeWebhook(token: AccessToken | undefined, webhookId: string) {
+ async deleteWebhook(token: AccessToken | undefined, webhookId: string) {
const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_WEBHOOK,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1587,12 +1893,15 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-tokenfamilies
*/
- async createTokenFamily(token: AccessToken | undefined, body: TalerMerchantApi.TokenFamilyCreateRequest) {
+ async createTokenFamily(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.TokenFamilyCreateRequest,
+ ) {
const url = new URL(`private/tokenfamilies`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -1601,12 +1910,18 @@ export class TalerMerchantInstanceHttpClient {
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1620,9 +1935,9 @@ export class TalerMerchantInstanceHttpClient {
) {
const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -1630,24 +1945,33 @@ export class TalerMerchantInstanceHttpClient {
headers,
});
switch (resp.status) {
- case HttpStatusCode.Ok:
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY,
+ );
return opSuccessFromHttp(resp, codecForTokenFamilyDetails());
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies
*/
- async listTokenFamilies(token: AccessToken | undefined,) {
+ async listTokenFamilies(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
const url = new URL(`private/tokenfamilies`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1657,22 +1981,27 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForTokenFamiliesList());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG
*/
- async getTokenFamily(token: AccessToken | undefined, tokenSlug: string) {
+ async getTokenFamilyDetails(
+ token: AccessToken | undefined,
+ tokenSlug: string,
+ ) {
const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1682,10 +2011,12 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForTokenFamilyDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1695,21 +2026,27 @@ export class TalerMerchantInstanceHttpClient {
async deleteTokenFamily(token: AccessToken | undefined, tokenSlug: string) {
const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1722,19 +2059,29 @@ export class TalerMerchantInstanceHttpClient {
getAuthenticationAPI(): URL {
return new URL(`private/`, this.baseUrl);
}
-
}
+export type TalerMerchantManagementResultByMethod<
+ prop extends keyof TalerMerchantManagementHttpClient,
+> = ResultByMethod<TalerMerchantManagementHttpClient, prop>;
+export type TalerMerchantManagementErrorsByMethod<
+ prop extends keyof TalerMerchantManagementHttpClient,
+> = FailCasesByMethod<TalerMerchantManagementHttpClient, prop>;
+
export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttpClient {
- readonly cacheManagementEvictor: CacheEvictor<TalerMerchantManagementCacheEviction>;
+ readonly cacheManagementEvictor: CacheEvictor<
+ TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
+ >;
constructor(
readonly baseUrl: string,
httpClient?: HttpRequestLibrary,
- cacheManagementEvictor?: CacheEvictor<TalerMerchantManagementCacheEviction>,
- cacheEvictor?: CacheEvictor<TalerMerchantInstanceCacheEviction>,
+ // cacheManagementEvictor?: CacheEvictor<TalerMerchantManagementCacheEviction>,
+ cacheEvictor?: CacheEvictor<
+ TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
+ >,
) {
super(baseUrl, httpClient, cacheEvictor);
- this.cacheManagementEvictor = cacheManagementEvictor ?? nullEvictor;
+ this.cacheManagementEvictor = cacheEvictor ?? nullEvictor;
}
getSubInstanceAPI(instanceId: string) {
@@ -1748,12 +2095,15 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
/**
* https://docs.taler.net/core/api-merchant.html#post--management-instances
*/
- async createInstance(token: AccessToken | undefined, body: TalerMerchantApi.InstanceConfigurationMessage) {
+ async createInstance(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.InstanceConfigurationMessage,
+ ) {
const url = new URL(`management/instances`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -1761,16 +2111,19 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
headers,
});
-
switch (resp.status) {
case HttpStatusCode.NoContent: {
- this.cacheManagementEvictor.notifySuccess(TalerMerchantManagementCacheEviction.CREATE_INSTANCE)
+ this.cacheManagementEvictor.notifySuccess(
+ TalerMerchantManagementCacheEviction.CREATE_INSTANCE,
+ );
return opEmptySuccess(resp);
}
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1782,11 +2135,14 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
instanceId: string,
body: TalerMerchantApi.InstanceAuthConfigurationMessage,
) {
- const url = new URL(`management/instances/${instanceId}/auth`, this.baseUrl);
+ const url = new URL(
+ `management/instances/${instanceId}/auth`,
+ this.baseUrl,
+ );
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
@@ -1797,10 +2153,12 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
switch (resp.status) {
case HttpStatusCode.NoContent:
return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1814,9 +2172,9 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
) {
const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "PATCH",
@@ -1824,24 +2182,33 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheManagementEvictor.notifySuccess(
+ TalerMerchantManagementCacheEviction.UPDATE_INSTANCE,
+ );
return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#get--management-instances
*/
- async listInstances(token: AccessToken | undefined,) {
+ async listInstances(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
const url = new URL(`management/instances`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1851,8 +2218,10 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForInstancesResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1860,12 +2229,12 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
* https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE
*
*/
- async getInstance(token: AccessToken | undefined, instanceId: string) {
+ async getInstanceDetails(token: AccessToken | undefined, instanceId: string) {
const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1875,40 +2244,52 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForQueryInstancesResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
/**
* https://docs.taler.net/core/api-merchant.html#delete--management-instances-$INSTANCE
*/
- async deleteInstance(token: AccessToken | undefined, instanceId: string, params: { purge?: boolean } = {}) {
+ async deleteInstance(
+ token: AccessToken | undefined,
+ instanceId: string,
+ params: { purge?: boolean } = {},
+ ) {
const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
- if (params.purge) {
- url.searchParams.set("purge", "YES");
+ if (params.purge !== undefined) {
+ url.searchParams.set("purge", params.purge ? "YES" : "NO");
}
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "DELETE",
headers,
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
+ case HttpStatusCode.NoContent: {
+ this.cacheManagementEvictor.notifySuccess(
+ TalerMerchantManagementCacheEviction.DELETE_INSTANCE,
+ );
return opEmptySuccess(resp);
- case HttpStatusCode.Unauthorized:
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
@@ -1932,9 +2313,9 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
url.searchParams.set("timeout_ms", String(params.timeout));
}
- const headers: Record<string, string> = {}
+ const headers: Record<string, string> = {};
if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token)
+ headers.Authorization = makeBearerTokenAuthHeader(token);
}
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
@@ -1945,6 +2326,10 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
return opSuccessFromHttp(resp, codecForAccountKycRedirects());
case HttpStatusCode.NoContent:
return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.BadGateway:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.ServiceUnavailable:
@@ -1952,7 +2337,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
- return opUnknownFailure(resp, await resp.text());
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
}
}
}
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
index 682b08984..c0004a218 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -18,6 +18,7 @@ import {
import { PaytoString, codecForPaytoString } from "../payto.js";
import {
AmountString,
+ InternationalizedString,
codecForInternationalizedString,
codecForLocation,
} from "../taler-types.js";
@@ -185,10 +186,54 @@ export interface LoginToken {
}
declare const __ac_token: unique symbol;
+/**
+ * Use `createAccessToken(string)` function to build one.
+ */
export type AccessToken = string & {
[__ac_token]: true;
};
+/**
+ * Create a rfc8959 access token.
+ * Adds secret-token: prefix if there is none.
+ * Encode the token with rfc7230 to send in a http header.
+ *
+ * @param token
+ * @returns
+ */
+export function createRFC8959AccessTokenEncoded(token: string): AccessToken {
+ return (
+ token.startsWith("secret-token:")
+ ? token
+ : `secret-token:${encodeURIComponent(token)}`
+ ) as AccessToken;
+}
+
+/**
+ * Create a rfc8959 access token.
+ * Adds secret-token: prefix if there is none.
+ *
+ * @param token
+ * @returns
+ */
+export function createRFC8959AccessTokenPlain(token: string): AccessToken {
+ return (
+ token.startsWith("secret-token:") ? token : `secret-token:${token}`
+ ) as AccessToken;
+}
+
+/**
+ * Convert string to access token.
+ *
+ * @param clientSecret
+ * @returns
+ */
+export function createClientSecretAccessToken(
+ clientSecret: string,
+): AccessToken {
+ return clientSecret as AccessToken;
+}
+
declare const __officer_signature: unique symbol;
export type OfficerSignature = string & {
[__officer_signature]: true;
@@ -295,6 +340,7 @@ export const codecForCoreBankConfig = (): Codec<TalerCorebankApi.Config> =>
.property("name", codecForConstString("libeufin-bank"))
.property("version", codecForString())
.property("bank_name", codecForString())
+ .property("base_url", codecForString())
.property("allow_conversion", codecForBoolean())
.property("allow_registrations", codecForBoolean())
.property("allow_deletions", codecForBoolean())
@@ -473,10 +519,10 @@ export const codecForQueryInstancesResponse =
.property(
"auth",
buildCodecForObject<{
- type: "external" | "token";
+ method: "external" | "token";
}>()
.property(
- "type",
+ "method",
codecForEither(
codecForConstString("token"),
codecForConstString("external"),
@@ -524,17 +570,24 @@ export const codecForAccountAddResponse =
export const codecForAccountsSummaryResponse =
(): Codec<TalerMerchantApi.AccountsSummaryResponse> =>
buildCodecForObject<TalerMerchantApi.AccountsSummaryResponse>()
- .property("accounts", codecForList(codecForBankAccountEntry()))
+ .property("accounts", codecForList(codecForBankAccountSummaryEntry()))
.build("TalerMerchantApi.AccountsSummaryResponse");
+export const codecForBankAccountSummaryEntry =
+ (): Codec<TalerMerchantApi.BankAccountSummaryEntry> =>
+ buildCodecForObject<TalerMerchantApi.BankAccountSummaryEntry>()
+ .property("payto_uri", codecForPaytoString())
+ .property("h_wire", codecForString())
+ .build("TalerMerchantApi.BankAccountSummaryEntry");
+
export const codecForBankAccountEntry =
(): Codec<TalerMerchantApi.BankAccountEntry> =>
buildCodecForObject<TalerMerchantApi.BankAccountEntry>()
.property("payto_uri", codecForPaytoString())
.property("h_wire", codecForString())
.property("salt", codecForString())
- .property("credit_facade_url", codecForURL())
- .property("active", codecForBoolean())
+ .property("credit_facade_url", codecOptional(codecForURL()))
+ .property("active", codecOptional(codecForBoolean()))
.build("TalerMerchantApi.BankAccountEntry");
export const codecForInventorySummaryResponse =
@@ -770,7 +823,7 @@ export const codecForTransferDetails =
.property("payto_uri", codecForPaytoString())
.property("exchange_url", codecForURL())
.property("transfer_serial_id", codecForNumber())
- .property("execution_time", codecForTimestamp)
+ .property("execution_time", codecOptional(codecForTimestamp))
.property("verified", codecOptional(codecForBoolean()))
.property("confirmed", codecOptional(codecForBoolean()))
.build("TalerMerchantApi.TransferDetails");
@@ -817,6 +870,11 @@ export const codecForTemplateDetails =
.property("template_description", codecForString())
.property("otp_id", codecOptional(codecForString()))
.property("template_contract", codecForTemplateContractDetails())
+ .property("required_currency", codecOptional(codecForString()))
+ .property(
+ "editable_defaults",
+ codecOptional(codecForTemplateContractDetailsDefaults()),
+ )
.build("TalerMerchantApi.TemplateDetails");
export const codecForTemplateContractDetails =
@@ -829,10 +887,25 @@ export const codecForTemplateContractDetails =
.property("pay_duration", codecForDuration)
.build("TalerMerchantApi.TemplateContractDetails");
+export const codecForTemplateContractDetailsDefaults =
+ (): Codec<TalerMerchantApi.TemplateContractDetailsDefaults> =>
+ buildCodecForObject<TalerMerchantApi.TemplateContractDetailsDefaults>()
+ .property("summary", codecOptional(codecForString()))
+ .property("currency", codecOptional(codecForString()))
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("minimum_age", codecOptional(codecForNumber()))
+ .property("pay_duration", codecOptional(codecForDuration))
+ .build("TalerMerchantApi.TemplateContractDetailsDefaults");
+
export const codecForWalletTemplateDetails =
(): Codec<TalerMerchantApi.WalletTemplateDetails> =>
buildCodecForObject<TalerMerchantApi.WalletTemplateDetails>()
.property("template_contract", codecForTemplateContractDetails())
+ .property("required_currency", codecOptional(codecForString()))
+ .property(
+ "editable_defaults",
+ codecOptional(codecForTemplateContractDetailsDefaults()),
+ )
.build("TalerMerchantApi.WalletTemplateDetails");
export const codecForWebhookSummaryResponse =
@@ -965,10 +1038,20 @@ export const codecForAccountMinimalData =
.property("name", codecForString())
.property("payto_uri", codecForPaytoString())
.property("balance", codecForBalance())
+ .property("row_id", codecForNumber())
.property("debit_threshold", codecForAmountString())
+ .property("min_cashout", codecOptional(codecForAmountString()))
.property("is_public", codecForBoolean())
.property("is_taler_exchange", codecForBoolean())
- .property("row_id", codecOptional(codecForNumber()))
+ .property(
+ "status",
+ codecOptional(
+ codecForEither(
+ codecForConstString("active"),
+ codecForConstString("deleted"),
+ ),
+ ),
+ )
.build("TalerCorebankApi.AccountMinimalData");
export const codecForListBankAccountsResponse =
@@ -983,6 +1066,7 @@ export const codecForAccountData = (): Codec<TalerCorebankApi.AccountData> =>
.property("balance", codecForBalance())
.property("payto_uri", codecForPaytoString())
.property("debit_threshold", codecForAmountString())
+ .property("min_cashout", codecOptional(codecForAmountString()))
.property("contact_data", codecOptional(codecForChallengeContactData()))
.property("cashout_payto_uri", codecOptional(codecForPaytoString()))
.property("is_public", codecForBoolean())
@@ -996,6 +1080,15 @@ export const codecForAccountData = (): Codec<TalerCorebankApi.AccountData> =>
),
),
)
+ .property(
+ "status",
+ codecOptional(
+ codecForEither(
+ codecForConstString("active"),
+ codecForConstString("deleted"),
+ ),
+ ),
+ )
.build("TalerCorebankApi.AccountData");
export const codecForChallengeContactData =
@@ -1211,26 +1304,33 @@ export const codecForBankWithdrawalOperationPostResponse =
.property("confirm_transfer_url", codecOptional(codecForURL()))
.build("TalerBankIntegrationApi.BankWithdrawalOperationPostResponse");
-export const codecForMerchantIncomingHistory =
- (): Codec<TalerRevenueApi.MerchantIncomingHistory> =>
- buildCodecForObject<TalerRevenueApi.MerchantIncomingHistory>()
+export const codecForRevenueConfig = (): Codec<TalerRevenueApi.RevenueConfig> =>
+ buildCodecForObject<TalerRevenueApi.RevenueConfig>()
+ .property("name", codecForConstString("taler-revenue"))
+ .property("version", codecForString())
+ .property("currency", codecForString())
+ .property("implementation", codecOptional(codecForString()))
+ .build("TalerRevenueApi.RevenueConfig");
+
+export const codecForRevenueIncomingHistory =
+ (): Codec<TalerRevenueApi.RevenueIncomingHistory> =>
+ buildCodecForObject<TalerRevenueApi.RevenueIncomingHistory>()
.property("credit_account", codecForPaytoString())
.property(
"incoming_transactions",
- codecForList(codecForMerchantIncomingBankTransaction()),
+ codecForList(codecForRevenueIncomingBankTransaction()),
)
.build("TalerRevenueApi.MerchantIncomingHistory");
-export const codecForMerchantIncomingBankTransaction =
- (): Codec<TalerRevenueApi.MerchantIncomingBankTransaction> =>
- buildCodecForObject<TalerRevenueApi.MerchantIncomingBankTransaction>()
- .property("row_id", codecForNumber())
- .property("date", codecForTimestamp)
+export const codecForRevenueIncomingBankTransaction =
+ (): Codec<TalerRevenueApi.RevenueIncomingBankTransaction> =>
+ buildCodecForObject<TalerRevenueApi.RevenueIncomingBankTransaction>()
.property("amount", codecForAmountString())
+ .property("date", codecForTimestamp)
.property("debit_account", codecForPaytoString())
- .property("exchange_url", codecForURL())
- .property("wtid", codecForString())
- .build("TalerRevenueApi.MerchantIncomingBankTransaction");
+ .property("row_id", codecForNumber())
+ .property("subject", codecForString())
+ .build("TalerRevenueApi.RevenueIncomingBankTransaction");
export const codecForTransferResponse =
(): Codec<TalerWireGatewayApi.TransferResponse> =>
@@ -1312,7 +1412,7 @@ export const codecForAddIncomingResponse =
export const codecForAmlRecords = (): Codec<TalerExchangeApi.AmlRecords> =>
buildCodecForObject<TalerExchangeApi.AmlRecords>()
.property("records", codecForList(codecForAmlRecord()))
- .build("TalerExchangeApi.PublicAccountsResponse");
+ .build("TalerExchangeApi.AmlRecords");
export const codecForAmlRecord = (): Codec<TalerExchangeApi.AmlRecord> =>
buildCodecForObject<TalerExchangeApi.AmlRecord>()
@@ -1382,51 +1482,6 @@ export const codecForAmlDecision = (): Codec<TalerExchangeApi.AmlDecision> =>
.property("kyc_requirements", codecOptional(codecForList(codecForString())))
.build("TalerExchangeApi.AmlDecision");
-// version: string;
-
-// // Name of the API.
-// name: "taler-conversion-info";
-
-// // Currency used by this bank.
-// regional_currency: string;
-
-// // How the bank SPA should render this currency.
-// regional_currency_specification: CurrencySpecification;
-
-// // External currency used during conversion.
-// fiat_currency: string;
-
-// // How the bank SPA should render this currency.
-// fiat_currency_specification: CurrencySpecification;
-
-// Extra conversion rate information.
-// // Only present if server opts in to report the static conversion rate.
-// conversion_info?: {
-
-// // Fee to subtract after applying the cashin ratio.
-// cashin_fee: AmountString;
-
-// // Fee to subtract after applying the cashout ratio.
-// cashout_fee: AmountString;
-
-// // Minimum amount authorised for cashin, in fiat before conversion
-// cashin_min_amount: AmountString;
-
-// // Minimum amount authorised for cashout, in regional before conversion
-// cashout_min_amount: AmountString;
-
-// // Smallest possible regional amount, converted amount is rounded to this amount
-// cashin_tiny_amount: AmountString;
-
-// // Smallest possible fiat amount, converted amount is rounded to this amount
-// cashout_tiny_amount: AmountString;
-
-// // Rounding mode used during cashin conversion
-// cashin_rounding_mode: "zero" | "up" | "nearest";
-
-// // Rounding mode used during cashout conversion
-// cashout_rounding_mode: "zero" | "up" | "nearest";
-// }
export const codecForConversionInfo =
(): Codec<TalerBankConversionApi.ConversionInfo> =>
buildCodecForObject<TalerBankConversionApi.ConversionInfo>()
@@ -1472,11 +1527,65 @@ export const codecForConversionBankConfig =
.property("conversion_rate", codecForConversionInfo())
.build("ConversionBankConfig.IntegrationConfig");
-// export const codecFor =
-// (): Codec<TalerWireGatewayApi.PublicAccountsResponse> =>
-// buildCodecForObject<TalerWireGatewayApi.PublicAccountsResponse>()
-// .property("", codecForString())
-// .build("TalerWireGatewayApi.PublicAccountsResponse");
+export const codecForChallengerTermsOfServiceResponse =
+ (): Codec<ChallengerApi.ChallengerTermsOfServiceResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerTermsOfServiceResponse>()
+ .property("name", codecForConstString("challenger"))
+ .property("version", codecForString())
+ .property("implementation", codecOptional(codecForString()))
+ .build("ChallengerApi.ChallengerTermsOfServiceResponse");
+
+export const codecForChallengeSetupResponse =
+ (): Codec<ChallengerApi.ChallengeSetupResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengeSetupResponse>()
+ .property("nonce", codecForString())
+ .build("ChallengerApi.ChallengeSetupResponse");
+
+export const codecForChallengeStatus =
+ (): Codec<ChallengerApi.ChallengeStatus> =>
+ buildCodecForObject<ChallengerApi.ChallengeStatus>()
+ .property("restrictions", codecOptional(codecForMap(codecForAny())))
+ .property("fix_address", codecForBoolean())
+ .property("last_address", codecOptional(codecForMap(codecForAny())))
+ .property("changes_left", codecForNumber())
+ .build("ChallengerApi.ChallengeStatus");
+export const codecForChallengeCreateResponse =
+ (): Codec<ChallengerApi.ChallengeCreateResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengeCreateResponse>()
+ .property("attempts_left", codecForNumber())
+ .property("address", codecForAny())
+ .property("transmitted", codecForBoolean())
+ .property("next_tx_time", codecForString())
+ .build("ChallengerApi.ChallengeCreateResponse");
+
+export const codecForInvalidPinResponse =
+ (): Codec<ChallengerApi.InvalidPinResponse> =>
+ buildCodecForObject<ChallengerApi.InvalidPinResponse>()
+ .property("ec", codecOptional(codecForNumber()))
+ .property("hint", codecForAny())
+ .property("addresses_left", codecForNumber())
+ .property("pin_transmissions_left", codecForNumber())
+ .property("auth_attempts_left", codecForNumber())
+ .property("exhausted", codecForBoolean())
+ .property("no_challenge", codecForBoolean())
+ .build("ChallengerApi.InvalidPinResponse");
+
+export const codecForChallengerAuthResponse =
+ (): Codec<ChallengerApi.ChallengerAuthResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerAuthResponse>()
+ .property("access_token", codecForString())
+ .property("token_type", codecForAny())
+ .property("expires_in", codecForNumber())
+ .build("ChallengerApi.ChallengerAuthResponse");
+
+export const codecForChallengerInfoResponse =
+ (): Codec<ChallengerApi.ChallengerInfoResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerInfoResponse>()
+ .property("id", codecForNumber())
+ .property("address", codecForAny())
+ .property("address_type", codecForString())
+ .property("expires", codecForTimestamp)
+ .build("ChallengerApi.ChallengerInfoResponse");
type EmailAddress = string;
type PhoneNumber = string;
@@ -1692,18 +1801,34 @@ export namespace TalerWireGatewayApi {
}
export namespace TalerRevenueApi {
- export interface MerchantIncomingHistory {
+ export interface RevenueConfig {
+ // Name of the API.
+ name: "taler-revenue";
+
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Currency used by this gateway.
+ currency: string;
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v0, may become mandatory in the future.
+ implementation?: string;
+ }
+
+ export interface RevenueIncomingHistory {
// Array of incoming transactions.
- incoming_transactions: MerchantIncomingBankTransaction[];
+ incoming_transactions: RevenueIncomingBankTransaction[];
// Payto URI to identify the receiver of funds.
- // This must be one of the merchant's bank accounts.
// Credit account is shared by all incoming transactions
// as per the nature of the request.
- credit_account: PaytoString;
+ credit_account: string;
}
- export interface MerchantIncomingBankTransaction {
+ export interface RevenueIncomingBankTransaction {
// Opaque identifier of the returned record.
row_id: SafeUint64;
@@ -1714,13 +1839,10 @@ export namespace TalerRevenueApi {
amount: AmountString;
// Payto URI to identify the sender of funds.
- debit_account: PaytoString;
+ debit_account: string;
- // Base URL of the exchange where the transfer originated form.
- exchange_url: string;
-
- // The wire transfer identifier.
- wtid: WireTransferIdentifierRawP;
+ // The wire transfer subject.
+ subject: string;
}
}
@@ -1835,6 +1957,7 @@ export namespace TalerBankConversionApi {
cashout_rounding_mode: RoundingMode;
}
}
+
export namespace TalerBankIntegrationApi {
export interface BankVersion {
// libtool-style representation of the Bank protocol version, see
@@ -1912,6 +2035,7 @@ export namespace TalerBankIntegrationApi {
confirm_transfer_url?: string;
}
}
+
export namespace TalerCorebankApi {
export interface IntegrationConfig {
// libtool-style representation of the Bank protocol version, see
@@ -1940,6 +2064,11 @@ export namespace TalerCorebankApi {
// @since v4, will become mandatory in the next version.
bank_name: string;
+ // Advertised base URL to use when you sharing an URL with another
+ // program.
+ // @since v4.
+ base_url?: string;
+
// If 'true' the server provides local currency conversion support
// If 'false' some parts of the API are not supported and return 501
allow_conversion: boolean;
@@ -2042,6 +2171,12 @@ export namespace TalerCorebankApi {
// query string parameter of the 'payto' field. In case it
// is given in both places, the paytoUri's takes the precedence.
amount?: AmountString;
+
+ // Nonce to make the request idempotent. Requests with the same
+ // request_uid that differ in any of the other fields
+ // are rejected.
+ // @since v4, will become mandatory in the next version.
+ request_uid?: ShortHashCode;
}
export interface CreateTransactionResponse {
@@ -2092,6 +2227,11 @@ export namespace TalerCorebankApi {
// Only admin can set this property.
debit_threshold?: AmountString;
+ // If present, set a custom minimum cashout amount for this account.
+ // Only admin can set this property
+ // @since v4
+ min_cashout?: AmountString;
+
// If present, enables 2FA and set the TAN channel used for challenges
// Only admin can set this property, other user can reconfig their account
// after creation.
@@ -2133,7 +2273,11 @@ export namespace TalerCorebankApi {
// Only admin can change this property.
debit_threshold?: AmountString;
- //FIX: missing in SPEC
+ // If present, change the custom minimum cashout amount for this account.
+ // Only admin can set this property
+ // @since v4
+ min_cashout?: AmountString;
+
// If present, enables 2FA and set the TAN channel used for challenges
tan_channel?: TanChannel | null;
}
@@ -2190,6 +2334,11 @@ export namespace TalerCorebankApi {
// Number indicating the max debit allowed for the requesting user.
debit_threshold: AmountString;
+ // Custom minimum cashout amount for this account.
+ // If null or absent, the global conversion fee is used.
+ // @since v4
+ min_cashout?: AmountString;
+
// Is this account visible to anyone?
is_public: boolean;
@@ -2199,6 +2348,14 @@ export namespace TalerCorebankApi {
// Opaque unique ID used for pagination.
// @since v4, will become mandatory in the future.
row_id?: Integer;
+
+ // Current status of the account
+ // active: the account can be used
+ // deleted: the account has been deleted but is retained for compliance
+ // reasons, only the administrator can access it
+ // Default to 'active' is missing
+ // @since v4, will become mandatory in the next version.
+ status?: "active" | "deleted";
}
export interface AccountData {
@@ -2214,6 +2371,11 @@ export namespace TalerCorebankApi {
// Number indicating the max debit allowed for the requesting user.
debit_threshold: AmountString;
+ // Custom minimum cashout amount for this account.
+ // If null or absent, the global conversion fee is used.
+ // @since v4
+ min_cashout?: AmountString;
+
contact_data?: ChallengeContactData;
// 'payto' address pointing the bank account
@@ -2232,6 +2394,14 @@ export namespace TalerCorebankApi {
// Is 2FA enabled and what channel is used for challenges?
tan_channel?: TanChannel;
+
+ // Current status of the account
+ // active: the account can be used
+ // deleted: the account has been deleted but is retained for compliance
+ // reasons, only the administrator can access it
+ // Default to 'active' is missing
+ // @since v4, will become mandatory in the next version.
+ status?: "active" | "deleted";
}
export interface CashoutRequest {
@@ -3551,7 +3721,7 @@ export namespace TalerMerchantApi {
// After the auth token has been set (with method "token"),
// the value must be provided in a "Authorization: Bearer $token"
// header.
- token?: string;
+ token?: AccessToken;
}
export interface InstanceReconfigurationMessage {
@@ -3672,7 +3842,7 @@ export namespace TalerMerchantApi {
// Authentication configuration.
// Does not contain the token when token auth is configured.
auth: {
- type: "external" | "token";
+ method: "external" | "token";
};
}
@@ -3772,7 +3942,16 @@ export namespace TalerMerchantApi {
export interface AccountsSummaryResponse {
// List of accounts that are known for the instance.
- accounts: BankAccountEntry[];
+ accounts: BankAccountSummaryEntry[];
+ }
+
+ // TODO: missing in docs
+ export interface BankAccountSummaryEntry {
+ // payto:// URI of the account.
+ payto_uri: PaytoString;
+
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
}
export interface BankAccountEntry {
// payto:// URI of the account.
@@ -3790,7 +3969,7 @@ export namespace TalerMerchantApi {
// true if this account is active,
// false if it is historic.
- active: boolean;
+ active?: boolean;
}
export interface ProductAddDetail {
@@ -4285,173 +4464,6 @@ export namespace TalerMerchantApi {
confirmed?: boolean;
}
- interface ReserveCreateRequest {
- // Amount that the merchant promises to put into the reserve.
- initial_balance: AmountString;
-
- // Exchange the merchant intends to use for rewards.
- exchange_url: string;
-
- // Desired wire method, for example "iban" or "x-taler-bank".
- wire_method: string;
- }
- interface ReserveCreateConfirmation {
- // Public key identifying the reserve.
- reserve_pub: EddsaPublicKey;
-
- // Wire accounts of the exchange where to transfer the funds.
- accounts: TalerExchangeApi.WireAccount[];
- }
-
- interface RewardReserveStatus {
- // Array of all known reserves (possibly empty!).
- reserves: ReserveStatusEntry[];
- }
- interface ReserveStatusEntry {
- // Public key of the reserve.
- reserve_pub: EddsaPublicKey;
-
- // Timestamp when it was established.
- creation_time: Timestamp;
-
- // Timestamp when it expires.
- expiration_time: Timestamp;
-
- // Initial amount as per reserve creation call.
- merchant_initial_amount: AmountString;
-
- // Initial amount as per exchange, 0 if exchange did
- // not confirm reserve creation yet.
- exchange_initial_amount: AmountString;
-
- // Amount picked up so far.
- pickup_amount: AmountString;
-
- // Amount approved for rewards that exceeds the pickup_amount.
- committed_amount: AmountString;
-
- // Is this reserve active (false if it was deleted but not purged)?
- active: boolean;
- }
-
- interface ReserveDetail {
- // Timestamp when it was established.
- creation_time: Timestamp;
-
- // Timestamp when it expires.
- expiration_time: Timestamp;
-
- // Initial amount as per reserve creation call.
- merchant_initial_amount: AmountString;
-
- // Initial amount as per exchange, 0 if exchange did
- // not confirm reserve creation yet.
- exchange_initial_amount: AmountString;
-
- // Amount picked up so far.
- pickup_amount: AmountString;
-
- // Amount approved for rewards that exceeds the pickup_amount.
- committed_amount: AmountString;
-
- // Array of all rewards created by this reserves (possibly empty!).
- // Only present if asked for explicitly.
- rewards?: RewardStatusEntry[];
-
- // Is this reserve active (false if it was deleted but not purged)?
- active: boolean;
-
- // Array of wire accounts of the exchange that could
- // be used to fill the reserve, can be NULL
- // if the reserve is inactive or was already filled
- accounts?: TalerExchangeApi.WireAccount[];
-
- // URL of the exchange hosting the reserve,
- // NULL if the reserve is inactive
- exchange_url: string;
- }
- interface RewardStatusEntry {
- // Unique identifier for the reward.
- reward_id: HashCode;
-
- // Total amount of the reward that can be withdrawn.
- total_amount: AmountString;
-
- // Human-readable reason for why the reward was granted.
- reason: string;
- }
-
- interface RewardCreateRequest {
- // Amount that the customer should be rewarded.
- amount: AmountString;
-
- // Justification for giving the reward.
- justification: string;
-
- // URL that the user should be directed to after receiving the reward,
- // will be included in the reward_token.
- next_url: string;
- }
- interface RewardCreateConfirmation {
- // Unique reward identifier for the reward that was created.
- reward_id: HashCode;
-
- // taler://reward URI for the reward.
- taler_reward_uri: string;
-
- // URL that will directly trigger processing
- // the reward when the browser is redirected to it.
- reward_status_url: string;
-
- // When does the reward expire?
- reward_expiration: Timestamp;
- }
-
- interface RewardDetails {
- // Amount that we authorized for this reward.
- total_authorized: AmountString;
-
- // Amount that was picked up by the user already.
- total_picked_up: AmountString;
-
- // Human-readable reason given when authorizing the reward.
- reason: string;
-
- // Timestamp indicating when the reward is set to expire (may be in the past).
- expiration: Timestamp;
-
- // Reserve public key from which the reward is funded.
- reserve_pub: EddsaPublicKey;
-
- // Array showing the pickup operations of the wallet (possibly empty!).
- // Only present if asked for explicitly.
- pickups?: PickupDetail[];
- }
- interface PickupDetail {
- // Unique identifier for the pickup operation.
- pickup_id: HashCode;
-
- // Number of planchets involved.
- num_planchets: Integer;
-
- // Total amount requested for this pickup_id.
- requested_amount: AmountString;
- }
-
- interface RewardsResponse {
- // List of rewards that are present in the backend.
- rewards: Reward[];
- }
- interface Reward {
- // ID of the reward in the backend database.
- row_id: number;
-
- // Unique identifier for the reward.
- reward_id: HashCode;
-
- // (Remaining) amount of the reward (including fees).
- reward_amount: AmountString;
- }
export interface OtpDeviceAddDetails {
// Device ID to use.
@@ -4569,6 +4581,23 @@ export namespace TalerMerchantApi {
// Additional information in a separate template.
template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
}
export interface TemplateContractDetails {
// Human-readable summary for the template.
@@ -4592,6 +4621,18 @@ export namespace TalerMerchantApi {
// It is deleted if the customer did not pay and if the duration is over.
pay_duration: RelativeTime;
}
+
+ export interface TemplateContractDetailsDefaults {
+ summary?: string;
+
+ currency?: string;
+
+ amount?: AmountString;
+
+ minimum_age?: Integer;
+
+ pay_duration?: RelativeTime;
+ }
export interface TemplatePatchDetails {
// Human-readable description for the template.
template_description: string;
@@ -4602,6 +4643,23 @@ export namespace TalerMerchantApi {
// Additional information in a separate template.
template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
}
export interface TemplateSummaryResponse {
@@ -4621,6 +4679,23 @@ export namespace TalerMerchantApi {
// Hard-coded information about the contrac terms
// for this template.
template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
}
export interface TemplateDetails {
@@ -4633,6 +4708,23 @@ export namespace TalerMerchantApi {
// Additional information in a separate template.
template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
}
export interface UsingTemplateDetails {
// Summary of the template
@@ -5067,3 +5159,132 @@ export namespace TalerMerchantApi {
master_pub: EddsaPublicKey;
}
}
+
+export namespace ChallengerApi {
+ export interface ChallengerTermsOfServiceResponse {
+ // Name of the service
+ name: "challenger";
+
+ // libtool-style representation of the Challenger protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v0, may become mandatory in the future.
+ implementation?: string;
+ }
+
+ export interface ChallengeSetupResponse {
+ // Nonce to use when constructing /authorize endpoint.
+ nonce: string;
+ }
+
+ export interface Restriction {
+ regex?: string;
+ hint?: string;
+ hint_i18n?: InternationalizedString;
+ }
+
+ export interface ChallengeStatus {
+ // Object; map of keys (names of the fields of the address
+ // to be entered by the user) to objects with a "regex" (string)
+ // containing an extended Posix regular expression for allowed
+ // address field values, and a "hint"/"hint_i18n" giving a
+ // human-readable explanation to display if the value entered
+ // by the user does not match the regex. Keys that are not mapped
+ // to such an object have no restriction on the value provided by
+ // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration.
+ restrictions: Record<string, Restriction> | undefined;
+
+ // indicates if the given address cannot be changed anymore, the
+ // form should be read-only if set to true.
+ fix_address: boolean;
+
+ // form values from the previous submission if available, details depend
+ // on the ADDRESS_TYPE, should be used to pre-populate the form
+ last_address: Record<string, string> | undefined;
+
+ // number of times the address can still be changed, may or may not be
+ // shown to the user
+ changes_left: Integer;
+ }
+
+ export interface ChallengeCreateResponse {
+ // how many more attempts are allowed, might be shown to the user,
+ // highlighting might be appropriate for low values such as 1 or 2 (the
+ // form will never be used if the value is zero)
+ attempts_left: Integer;
+
+ // the address that is being validated, might be shown or not
+ address: Object;
+
+ // true if we just retransmitted the challenge, false if we sent a
+ // challenge recently and thus refused to transmit it again this time;
+ // might make a useful hint to the user
+ transmitted: boolean;
+
+ // timestamp explaining when we would re-transmit the challenge the next
+ // time (at the earliest) if requested by the user
+ next_tx_time: string;
+ }
+
+ export interface InvalidPinResponse {
+ // numeric Taler error code, should be shown to indicate the error
+ // compactly for reporting to developers
+ ec?: number;
+
+ // human-readable Taler error code, should be shown for the user to
+ // understand the error
+ hint: string;
+
+ // how many times is the user still allowed to change the address;
+ // if 0, the user should not be shown a link to jump to the
+ // address entry form
+ addresses_left: Integer;
+
+ // how many times might the PIN still be retransmitted
+ pin_transmissions_left: Integer;
+
+ // how many times might the user still try entering the PIN code
+ auth_attempts_left: Integer;
+
+ // if true, the PIN was not even evaluated as the user previously
+ // exhausted the number of attempts
+ exhausted: boolean;
+
+ // if true, the PIN was not even evaluated as no challenge was ever
+ // issued (the user must have skipped the step of providing their
+ // address first!)
+ no_challenge: boolean;
+ }
+
+ export interface ChallengerAuthResponse {
+ // Token used to authenticate access in /info.
+ access_token: string;
+
+ // Type of the access token.
+ token_type: "Bearer";
+
+ // Amount of time that an access token is valid (in seconds).
+ expires_in: Integer;
+ }
+
+ export interface ChallengerInfoResponse {
+ // Unique ID of the record within Challenger
+ // (identifies the rowid of the token).
+ id: Integer;
+
+ // Address that was validated.
+ // Key-value pairs, details depend on the
+ // address_type.
+ address: Object;
+
+ // Type of the address.
+ address_type: string;
+
+ // How long do we consider the address to be
+ // valid for this user.
+ expires: Timestamp;
+ }
+}
diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts
index d6623cf00..bf186ce46 100644
--- a/packages/taler-util/src/http-client/utils.ts
+++ b/packages/taler-util/src/http-client/utils.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import { base64FromArrayBuffer } from "../base64.js";
-import { stringToBytes } from "../taler-crypto.js";
+import { encodeCrock, getRandomBytes, stringToBytes } from "../taler-crypto.js";
import { AccessToken, LongPollParams, PaginationParams } from "./types.js";
/**
@@ -39,7 +39,7 @@ export function makeBasicAuthHeader(
* @returns
*/
export function makeBearerTokenAuthHeader(token: AccessToken): string {
- return `Bearer secret-token:${token}`;
+ return `Bearer ${token}`;
}
/**
@@ -90,3 +90,27 @@ export interface CacheEvictor<T> {
export const nullEvictor: CacheEvictor<unknown> = {
notifySuccess: () => Promise.resolve(),
};
+
+export class IdempotencyRetry {
+ public readonly uid: string;
+ public readonly timesLeft: number;
+ public readonly maxTries: number;
+
+ private constructor(timesLeft: number, maxTimesLeft: number) {
+ this.timesLeft = timesLeft;
+ this.maxTries = maxTimesLeft;
+ this.uid = encodeCrock(getRandomBytes(32))
+ }
+
+ static tryFiveTimes() {
+ return new IdempotencyRetry(5, 5)
+ }
+
+ next(): IdempotencyRetry | undefined {
+ const left = this.timesLeft -1
+ if (left <= 0) {
+ return undefined
+ }
+ return new IdempotencyRetry(left, this.maxTries);
+ }
+}
diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts
index cc75debd5..d8cd36287 100644
--- a/packages/taler-util/src/http-common.ts
+++ b/packages/taler-util/src/http-common.ts
@@ -268,6 +268,47 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
};
}
+export async function readResponseJsonOrErrorCode<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<{ isError: boolean; response: T }> {
+ let respJson;
+ try {
+ respJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from response",
+ );
+ }
+ let parsedResponse: T;
+ try {
+ parsedResponse = codec.decode(respJson);
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Response invalid",
+ );
+ }
+ return {
+ isError: !(httpResponse.status >= 200 && httpResponse.status < 300),
+ response: parsedResponse,
+ };
+}
+
+
type HttpErrorDetails = {
requestUrl: string;
requestMethod: string;
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
index 8606bc451..45a12c258 100644
--- a/packages/taler-util/src/http-impl.node.ts
+++ b/packages/taler-util/src/http-impl.node.ts
@@ -123,8 +123,8 @@ export class HttpLibImpl implements HttpRequestLibrary {
if (opt?.headers) {
Object.entries(opt?.headers).forEach(([key, value]) => {
if (value === undefined) return;
- requestHeadersMap[key] = value
- })
+ requestHeadersMap[key] = value;
+ });
}
logger.trace(`request timeout ${timeoutMs} ms`);
@@ -181,10 +181,10 @@ export class HttpLibImpl implements HttpRequestLibrary {
return arg + " '" + String(v) + "'";
}
console.log(
- `curl -X ${options.method} ${parsedUrl.href} ${ifUndefined(
+ `curl -X ${options.method} "${parsedUrl.href}" ${headers} ${ifUndefined(
"-d",
payload,
- )} ${headers}`,
+ )}`,
);
}
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index 9bd4834d2..24d6e9950 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -22,6 +22,7 @@ export * from "./http-client/bank-conversion.js";
export * from "./http-client/authentication.js";
export * from "./http-client/bank-core.js";
export * from "./http-client/merchant.js";
+export * from "./http-client/challenger.js";
export * from "./http-client/bank-integration.js";
export * from "./http-client/bank-revenue.js";
export * from "./http-client/bank-wire.js";
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index f439b4a6f..d4dfe7589 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -22,7 +22,6 @@
/**
* Imports.
*/
-import { CancellationToken } from "./CancellationToken.js";
import { AbsoluteTime } from "./time.js";
import { TransactionState } from "./transactions-types.js";
import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js";
@@ -31,8 +30,12 @@ export enum NotificationType {
BalanceChange = "balance-change",
BackupOperationError = "backup-error",
TransactionStateTransition = "transaction-state-transition",
+ /**
+ * @deprecated
+ */
WithdrawalOperationTransition = "withdrawal-operation-transition",
ExchangeStateTransition = "exchange-state-transition",
+ Idle = "idle",
TaskObservabilityEvent = "task-observability-event",
RequestObservabilityEvent = "request-observability-event",
}
@@ -130,6 +133,7 @@ export enum ObservabilityEventType {
CryptoStart = "crypto-start",
CryptoFinishSuccess = "crypto-finish-success",
CryptoFinishError = "crypto-finish-error",
+ Message = "message",
}
export type ObservabilityEvent =
@@ -173,6 +177,7 @@ export type ObservabilityEvent =
}
| {
type: ObservabilityEventType.RequestFinishSuccess;
+ durationMs: number;
}
| {
type: ObservabilityEventType.RequestFinishError;
@@ -208,6 +213,10 @@ export type ObservabilityEvent =
| {
type: ObservabilityEventType.ShepherdTaskResult;
resultType: string;
+ }
+ | {
+ type: ObservabilityEventType.Message;
+ contents: string;
};
export interface BackupOperationErrorNotification {
@@ -225,6 +234,10 @@ export interface WithdrawalOperationTransitionNotification {
uri: string;
}
+export interface IdleNotification {
+ type: NotificationType.Idle;
+}
+
export type WalletNotification =
| BalanceChangeNotification
| WithdrawalOperationTransitionNotification
@@ -232,4 +245,5 @@ export type WalletNotification =
| ExchangeStateTransitionNotification
| TransactionStateTransitionNotification
| TaskProgressNotification
- | RequestProgressNotification;
+ | RequestProgressNotification
+ | IdleNotification;
diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts
index 07a216fe9..e2ab9d4e4 100644
--- a/packages/taler-util/src/operation.ts
+++ b/packages/taler-util/src/operation.ts
@@ -19,6 +19,7 @@
*/
import {
HttpResponse,
+ readResponseJsonOrErrorCode,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "./http-common.js";
@@ -126,7 +127,7 @@ export async function opKnownAlternativeFailure<T extends HttpStatusCode, B>(
s: T,
codec: Codec<B>,
): Promise<OperationAlternative<T, B>> {
- const body = await readSuccessResponseJsonOrThrow(resp, codec);
+ const body = (await readResponseJsonOrErrorCode(resp, codec)).response;
return { type: "fail", case: s, body };
}
@@ -138,22 +139,21 @@ export async function opKnownHttpFailure<T extends HttpStatusCode>(
return { type: "fail", case: s, detail };
}
-export async function opKnownTalerFailure<T extends TalerErrorCode>(
+export function opKnownTalerFailure<T extends TalerErrorCode>(
s: T,
- resp: HttpResponse,
-): Promise<OperationFail<T>> {
- const detail = await readTalerErrorResponse(resp);
+ detail: TalerErrorDetail,
+): OperationFail<T> {
return { type: "fail", case: s, detail };
}
-export function opUnknownFailure(resp: HttpResponse, text: string): never {
+export function opUnknownFailure(resp: HttpResponse, error: TalerErrorDetail): never {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
requestUrl: resp.requestUrl,
requestMethod: resp.requestMethod,
httpStatusCode: resp.status,
- errorResponse: text,
+ errorResponse: error,
},
`Unexpected HTTP status ${resp.status} in response`,
);
@@ -184,13 +184,15 @@ export type ResultByMethod<
p extends keyof TT,
> = TT[p] extends (...args: any[]) => infer Ret
? Ret extends Promise<infer Result>
- ? Result extends OperationResult<any, any>
- ? Result
- : never
- : never //api always use Promises
+ ? Result extends OperationResult<any, any>
+ ? Result
+ : never
+ : never //api always use Promises
: never; //error cases just for functions
export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude<
ResultByMethod<TT, p>,
OperationOk<any>
>;
+
+export type RedirectResult = { redirectURL: URL }
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
index e587773e2..950161b10 100644
--- a/packages/taler-util/src/taler-crypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -21,23 +21,23 @@
/**
* Imports.
*/
-import * as nacl from "./nacl-fast.js";
-import { hmacSha256, hmacSha512 } from "./kdf.js";
import bigint from "big-integer";
+import * as fflate from "fflate";
+import { AmountLike, Amounts } from "./amounts.js";
import * as argon2 from "./argon2.js";
+import { canonicalJson } from "./helpers.js";
+import { hmacSha256, hmacSha512 } from "./kdf.js";
+import { Logger } from "./logging.js";
+import * as nacl from "./nacl-fast.js";
+import { secretbox } from "./nacl-fast.js";
import {
CoinEnvelope,
CoinPublicKeyString,
- DenominationPubKey,
DenomKeyType,
+ DenominationPubKey,
HashCodeString,
} from "./taler-types.js";
-import { Logger } from "./logging.js";
-import { secretbox } from "./nacl-fast.js";
-import * as fflate from "fflate";
-import { canonicalJson } from "./helpers.js";
import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
-import { AmountLike, Amounts } from "./amounts.js";
export type Flavor<T, FlavorT extends string> = T & {
_flavor?: `taler.${FlavorT}`;
@@ -974,6 +974,7 @@ export function hashWire(paytoUri: string, salt: string): string {
export enum TalerSignaturePurpose {
MERCHANT_TRACK_TRANSACTION = 1103,
WALLET_RESERVE_WITHDRAW = 1200,
+ WALLET_RESERVE_HISTORY = 1208,
WALLET_COIN_DEPOSIT = 1201,
GLOBAL_FEES = 1022,
MASTER_DENOMINATION_KEY_VALIDITY = 1025,
diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
index 2c65255df..9985e74b3 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -22,6 +22,8 @@
*/
export enum TalerErrorCode {
+
+
/**
* Special code to indicate success (no error).
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -29,6 +31,7 @@ export enum TalerErrorCode {
*/
NONE = 0,
+
/**
* An error response did not include an error code in the format expected by the client. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -36,6 +39,7 @@ export enum TalerErrorCode {
*/
INVALID = 1,
+
/**
* An internal failure happened on the client side. Details should be in the local logs. Check if you are using the latest available version or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -43,6 +47,7 @@ export enum TalerErrorCode {
*/
GENERIC_CLIENT_INTERNAL_ERROR = 2,
+
/**
* The response we got from the server was not in the expected format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -50,6 +55,7 @@ export enum TalerErrorCode {
*/
GENERIC_INVALID_RESPONSE = 10,
+
/**
* The operation timed out. Trying again might help. Check the network connection.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -57,6 +63,7 @@ export enum TalerErrorCode {
*/
GENERIC_TIMEOUT = 11,
+
/**
* The protocol version given by the server does not follow the required format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -64,6 +71,7 @@ export enum TalerErrorCode {
*/
GENERIC_VERSION_MALFORMED = 12,
+
/**
* The service responded with a reply that was in the right data format, but the content did not satisfy the protocol. Please file a bug report.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -71,6 +79,7 @@ export enum TalerErrorCode {
*/
GENERIC_REPLY_MALFORMED = 13,
+
/**
* There is an error in the client-side configuration, for example an option is set to an invalid value. Check the logs and fix the local configuration.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -78,6 +87,7 @@ export enum TalerErrorCode {
*/
GENERIC_CONFIGURATION_INVALID = 14,
+
/**
* The client made a request to a service, but received an error response it does not know how to handle. Please file a bug report.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -85,6 +95,7 @@ export enum TalerErrorCode {
*/
GENERIC_UNEXPECTED_REQUEST_ERROR = 15,
+
/**
* The token used by the client to authorize the request does not grant the required permissions for the request. Check the requirements and obtain a suitable authorization token to proceed.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -92,6 +103,7 @@ export enum TalerErrorCode {
*/
GENERIC_TOKEN_PERMISSION_INSUFFICIENT = 16,
+
/**
* The HTTP method used is invalid for this endpoint. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405).
@@ -99,6 +111,7 @@ export enum TalerErrorCode {
*/
GENERIC_METHOD_INVALID = 20,
+
/**
* There is no endpoint defined for the URL provided by the client. Check if you used the correct URL and/or file a report with the developers of the client software.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -106,6 +119,7 @@ export enum TalerErrorCode {
*/
GENERIC_ENDPOINT_UNKNOWN = 21,
+
/**
* The JSON in the client's request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -113,6 +127,7 @@ export enum TalerErrorCode {
*/
GENERIC_JSON_INVALID = 22,
+
/**
* Some of the HTTP headers provided by the client were malformed and caused the server to not be able to handle the request. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -120,6 +135,7 @@ export enum TalerErrorCode {
*/
GENERIC_HTTP_HEADERS_MALFORMED = 23,
+
/**
* The payto:// URI provided by the client is malformed. Check that you are using the correct syntax as of RFC 8905 and/or that you entered the bank account number correctly.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -127,6 +143,7 @@ export enum TalerErrorCode {
*/
GENERIC_PAYTO_URI_MALFORMED = 24,
+
/**
* A required parameter in the request was missing. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -134,6 +151,7 @@ export enum TalerErrorCode {
*/
GENERIC_PARAMETER_MISSING = 25,
+
/**
* A parameter in the request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -141,6 +159,7 @@ export enum TalerErrorCode {
*/
GENERIC_PARAMETER_MALFORMED = 26,
+
/**
* The reserve public key was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -148,6 +167,7 @@ export enum TalerErrorCode {
*/
GENERIC_RESERVE_PUB_MALFORMED = 27,
+
/**
* The body in the request could not be decompressed by the server. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -155,6 +175,7 @@ export enum TalerErrorCode {
*/
GENERIC_COMPRESSION_INVALID = 28,
+
/**
* The currency involved in the operation is not acceptable for this server. Check your configuration and make sure the currency specified for a given service provider is one of the currencies supported by that provider.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -162,6 +183,7 @@ export enum TalerErrorCode {
*/
GENERIC_CURRENCY_MISMATCH = 30,
+
/**
* The URI is longer than the longest URI the HTTP server is willing to parse. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit.
* Returned with an HTTP status code of #MHD_HTTP_URI_TOO_LONG (414).
@@ -169,6 +191,7 @@ export enum TalerErrorCode {
*/
GENERIC_URI_TOO_LONG = 31,
+
/**
* The body is too large to be permissible for the endpoint. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit.
* Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
@@ -176,6 +199,7 @@ export enum TalerErrorCode {
*/
GENERIC_UPLOAD_EXCEEDS_LIMIT = 32,
+
/**
* The service refused the request due to lack of proper authorization.
* Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
@@ -183,6 +207,7 @@ export enum TalerErrorCode {
*/
GENERIC_UNAUTHORIZED = 40,
+
/**
* The service refused the request as the given authorization token is unknown.
* Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
@@ -190,6 +215,7 @@ export enum TalerErrorCode {
*/
GENERIC_TOKEN_UNKNOWN = 41,
+
/**
* The service refused the request as the given authorization token expired.
* Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
@@ -197,6 +223,7 @@ export enum TalerErrorCode {
*/
GENERIC_TOKEN_EXPIRED = 42,
+
/**
* The service refused the request as the given authorization token is malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
@@ -204,6 +231,7 @@ export enum TalerErrorCode {
*/
GENERIC_TOKEN_MALFORMED = 43,
+
/**
* The service refused the request due to lack of proper rights on the resource.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -211,6 +239,7 @@ export enum TalerErrorCode {
*/
GENERIC_FORBIDDEN = 44,
+
/**
* The service failed initialize its connection to the database. The system administrator should check that the service has permissions to access the database and that the database is running.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -218,6 +247,7 @@ export enum TalerErrorCode {
*/
GENERIC_DB_SETUP_FAILED = 50,
+
/**
* The service encountered an error event to just start the database transaction. The system administrator should check that the database is running.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -225,6 +255,7 @@ export enum TalerErrorCode {
*/
GENERIC_DB_START_FAILED = 51,
+
/**
* The service failed to store information in its database. The system administrator should check that the database is running and review the service logs.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -232,6 +263,7 @@ export enum TalerErrorCode {
*/
GENERIC_DB_STORE_FAILED = 52,
+
/**
* The service failed to fetch information from its database. The system administrator should check that the database is running and review the service logs.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -239,6 +271,7 @@ export enum TalerErrorCode {
*/
GENERIC_DB_FETCH_FAILED = 53,
+
/**
* The service encountered an unrecoverable error trying to commit a transaction to the database. The system administrator should check that the database is running and review the service logs.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -246,6 +279,7 @@ export enum TalerErrorCode {
*/
GENERIC_DB_COMMIT_FAILED = 54,
+
/**
* The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. This indicates a repeated serialization error; it should only happen if some client maliciously tries to create conflicting concurrent transactions. It could also be a sign of a missing index. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -253,6 +287,7 @@ export enum TalerErrorCode {
*/
GENERIC_DB_SOFT_FAILURE = 55,
+
/**
* The service's database is inconsistent and violates service-internal invariants. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -260,6 +295,7 @@ export enum TalerErrorCode {
*/
GENERIC_DB_INVARIANT_FAILURE = 56,
+
/**
* The HTTP server experienced an internal invariant failure (bug). Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -267,6 +303,7 @@ export enum TalerErrorCode {
*/
GENERIC_INTERNAL_INVARIANT_FAILURE = 60,
+
/**
* The service could not compute a cryptographic hash over some JSON value. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -274,6 +311,7 @@ export enum TalerErrorCode {
*/
GENERIC_FAILED_COMPUTE_JSON_HASH = 61,
+
/**
* The service could not compute an amount. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -281,6 +319,7 @@ export enum TalerErrorCode {
*/
GENERIC_FAILED_COMPUTE_AMOUNT = 62,
+
/**
* The HTTP server had insufficient memory to parse the request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -288,6 +327,7 @@ export enum TalerErrorCode {
*/
GENERIC_PARSER_OUT_OF_MEMORY = 70,
+
/**
* The HTTP server failed to allocate memory. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -295,6 +335,7 @@ export enum TalerErrorCode {
*/
GENERIC_ALLOCATION_FAILURE = 71,
+
/**
* The HTTP server failed to allocate memory for building JSON reply. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -302,6 +343,7 @@ export enum TalerErrorCode {
*/
GENERIC_JSON_ALLOCATION_FAILURE = 72,
+
/**
* The HTTP server failed to allocate memory for making a CURL request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -309,6 +351,7 @@ export enum TalerErrorCode {
*/
GENERIC_CURL_ALLOCATION_FAILURE = 73,
+
/**
* The backend could not locate a required template to generate an HTML reply. The system administrator should check if the resource files are installed in the correct location and are readable to the service.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -316,6 +359,7 @@ export enum TalerErrorCode {
*/
GENERIC_FAILED_TO_LOAD_TEMPLATE = 74,
+
/**
* The backend could not expand the template to generate an HTML reply. The system administrator should investigate the logs and check if the templates are well-formed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -323,6 +367,7 @@ export enum TalerErrorCode {
*/
GENERIC_FAILED_TO_EXPAND_TEMPLATE = 75,
+
/**
* Exchange is badly configured and thus cannot operate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -330,6 +375,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_BAD_CONFIGURATION = 1000,
+
/**
* Operation specified unknown for this endpoint.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -337,6 +383,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_OPERATION_UNKNOWN = 1001,
+
/**
* The number of segments included in the URI does not match the number of segments expected by the endpoint.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -344,6 +391,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS = 1002,
+
/**
* The same coin was already used with a different denomination previously.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -351,6 +399,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY = 1003,
+
/**
* The public key of given to a "/coins/" endpoint of the exchange was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -358,6 +407,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COINS_INVALID_COIN_PUB = 1004,
+
/**
* The exchange is not aware of the denomination key the wallet requested for the operation.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -365,6 +415,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN = 1005,
+
/**
* The signature of the denomination key over the coin is not valid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -372,6 +423,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DENOMINATION_SIGNATURE_INVALID = 1006,
+
/**
* The exchange failed to perform the operation as it could not find the private keys. This is a problem with the exchange setup, not with the client's request.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
@@ -379,6 +431,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_KEYS_MISSING = 1007,
+
/**
* Validity period of the denomination lies in the future.
* Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
@@ -386,6 +439,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE = 1008,
+
/**
* Denomination key of the coin is past its expiration time for the requested operation.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -393,6 +447,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_EXPIRED = 1009,
+
/**
* Denomination key of the coin has been revoked.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -400,6 +455,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_REVOKED = 1010,
+
/**
* An operation where the exchange interacted with a security module timed out.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -407,6 +463,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_SECMOD_TIMEOUT = 1011,
+
/**
* The respective coin did not have sufficient residual value for the operation. The "history" in this response provides the "residual_value" of the coin, which may be less than its "original_value".
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -414,6 +471,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_INSUFFICIENT_FUNDS = 1012,
+
/**
* The exchange had an internal error reconstructing the transaction history of the coin that was being processed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -421,6 +479,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COIN_HISTORY_COMPUTATION_FAILED = 1013,
+
/**
* The exchange failed to obtain the transaction history of the given coin from the database while generating an insufficient funds errors.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -428,6 +487,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS = 1014,
+
/**
* The same coin was already used with a different age hash previously.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -435,6 +495,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH = 1015,
+
/**
* The requested operation is not valid for the cipher used by the selected denomination.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -442,6 +503,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION = 1016,
+
/**
* The provided arguments for the operation use inconsistent ciphers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -449,6 +511,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_CIPHER_MISMATCH = 1017,
+
/**
* The number of denominations specified in the request exceeds the limit of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -456,6 +519,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1018,
+
/**
* The coin is not known to the exchange (yet).
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -463,6 +527,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COIN_UNKNOWN = 1019,
+
/**
* The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -470,6 +535,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_CLOCK_SKEW = 1020,
+
/**
* The specified amount for the coin is higher than the value of the denomination of the coin.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -477,6 +543,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE = 1021,
+
/**
* The exchange was not properly configured with global fees.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -484,6 +551,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_GLOBAL_FEES_MISSING = 1022,
+
/**
* The exchange was not properly configured with wire fees.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -491,6 +559,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_WIRE_FEES_MISSING = 1023,
+
/**
* The purse public key was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -498,6 +567,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_PURSE_PUB_MALFORMED = 1024,
+
/**
* The purse is unknown.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -505,6 +575,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_PURSE_UNKNOWN = 1025,
+
/**
* The purse has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -512,6 +583,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_PURSE_EXPIRED = 1026,
+
/**
* The exchange has no information about the "reserve_pub" that was given.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -519,6 +591,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_RESERVE_UNKNOWN = 1027,
+
/**
* The exchange is not allowed to proceed with the operation until the client has satisfied a KYC check.
* Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
@@ -526,6 +599,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_KYC_REQUIRED = 1028,
+
/**
* Inconsistency between provided age commitment and attest: either none or both must be provided
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -533,6 +607,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_COIN_CONFLICTING_ATTEST_VS_AGE_COMMITMENT = 1029,
+
/**
* The provided attestation for the minimum age couldn't be verified by the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -540,6 +615,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_COIN_AGE_ATTESTATION_FAILURE = 1030,
+
/**
* The purse was deleted.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -547,6 +623,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_PURSE_DELETED = 1031,
+
/**
* The public key of the AML officer in the URL was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -554,6 +631,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_AML_OFFICER_PUB_MALFORMED = 1032,
+
/**
* The signature affirming the GET request of the AML officer is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -561,6 +639,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_AML_OFFICER_GET_SIGNATURE_INVALID = 1033,
+
/**
* The specified AML officer does not have access at this time.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -568,6 +647,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_AML_OFFICER_ACCESS_DENIED = 1034,
+
/**
* The requested operation is denied pending the resolution of an anti-money laundering investigation by the exchange operator. This is a manual process, please wait and retry later.
* Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
@@ -575,6 +655,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_AML_PENDING = 1035,
+
/**
* The requested operation is denied as the account was frozen on suspicion of money laundering. Please contact the exchange operator.
* Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
@@ -582,6 +663,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_AML_FROZEN = 1036,
+
/**
* The exchange failed to start a KYC attribute conversion helper process. It is likely configured incorrectly.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -589,6 +671,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_KYC_CONVERTER_FAILED = 1037,
+
/**
* The exchange did not find information about the specified transaction in the database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -596,6 +679,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_NOT_FOUND = 1100,
+
/**
* The wire hash of given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -603,6 +687,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE = 1101,
+
/**
* The merchant key of given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -610,6 +695,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB = 1102,
+
/**
* The hash of the contract terms given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -617,6 +703,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS = 1103,
+
/**
* The coin public key of given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -624,6 +711,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB = 1104,
+
/**
* The signature returned by the exchange in a /deposits/ request was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -631,6 +719,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE = 1105,
+
/**
* The signature of the merchant is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -638,6 +727,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID = 1106,
+
/**
* The provided policy data was not accepted
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -645,6 +735,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_POLICY_NOT_ACCEPTED = 1107,
+
/**
* The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -652,6 +743,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS = 1150,
+
/**
* The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -659,6 +751,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS = 1151,
+
/**
* The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -666,6 +759,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW = 1152,
+
/**
* The exchange failed to create the signature using the denomination key.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -673,6 +767,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_SIGNATURE_FAILED = 1153,
+
/**
* The signature of the reserve is not valid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -680,6 +775,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID = 1154,
+
/**
* When computing the reserve history, we ended up with a negative overall balance, which should be impossible.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -687,6 +783,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVE_HISTORY_ERROR_INSUFFICIENT_FUNDS = 1155,
+
/**
* The reserve did not have sufficient funds in it to pay for a full reserve history statement.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -694,6 +791,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GET_RESERVE_HISTORY_ERROR_INSUFFICIENT_BALANCE = 1156,
+
/**
* Withdraw period of the coin to be withdrawn is in the past.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -701,6 +799,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_DENOMINATION_KEY_LOST = 1158,
+
/**
* The client failed to unblind the blind signature.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -708,6 +807,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_UNBLIND_FAILURE = 1159,
+
/**
* The client re-used a withdraw nonce, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -715,6 +815,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_NONCE_REUSE = 1160,
+
/**
* The client provided an unknown commitment for an age-withdraw request.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -722,6 +823,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AGE_WITHDRAW_COMMITMENT_UNKNOWN = 1161,
+
/**
* The total sum of amounts from the denominations did overflow.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -729,6 +831,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW = 1162,
+
/**
* The total sum of value and fees from the denominations differs from the committed amount with fees.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -736,6 +839,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT = 1163,
+
/**
* The original commitment differs from the calculated hash
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -743,6 +847,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH = 1164,
+
/**
* The maximum age in the commitment is too large for the reserve
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -750,6 +855,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE = 1165,
+
/**
* The batch withdraw included a planchet that was already withdrawn. This is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -757,6 +863,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET = 1175,
+
/**
* The signature made by the coin over the deposit permission is not valid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -764,6 +871,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID = 1205,
+
/**
* The same coin was already deposited for the same merchant and contract with other details.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -771,6 +879,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT = 1206,
+
/**
* The stated value of the coin after the deposit fee is subtracted would be negative.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -778,6 +887,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE = 1207,
+
/**
* The stated refund deadline is after the wire deadline.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -785,6 +895,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE = 1208,
+
/**
* The stated wire deadline is "never", which makes no sense.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -792,6 +903,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER = 1209,
+
/**
* The exchange failed to canonicalize and hash the given wire format. For example, the merchant failed to provide the "salt" or a valid payto:// URI in the wire details. Note that while the exchange will do some basic sanity checking on the wire details, it cannot warrant that the banking system will ultimately be able to route to the specified address, even if this check passed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -799,6 +911,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_JSON = 1210,
+
/**
* The hash of the given wire address does not match the wire hash specified in the proposal data.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -806,6 +919,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_CONTRACT_HASH_CONFLICT = 1211,
+
/**
* The signature provided by the exchange is not valid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -813,6 +927,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE = 1221,
+
/**
* The deposited amount is smaller than the deposit fee, which would result in a negative contribution.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -820,6 +935,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT = 1222,
+
/**
* The proof of policy fulfillment was invalid.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -827,6 +943,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_EXTENSIONS_INVALID_FULFILLMENT = 1240,
+
/**
* The coin history was requested with a bad signature.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -834,6 +951,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_COIN_HISTORY_BAD_SIGNATURE = 1251,
+
/**
* The reserve history was requested with a bad signature.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -841,6 +959,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE = 1252,
+
/**
* The exchange encountered melt fees exceeding the melted coin's contribution.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -848,6 +967,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION = 1302,
+
/**
* The signature made with the coin to be melted is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -855,6 +975,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_COIN_SIGNATURE_INVALID = 1303,
+
/**
* The denomination of the given coin has past its expiration date and it is also not a valid zombie (that is, was not refreshed with the fresh coin being subjected to recoup).
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -862,6 +983,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE = 1305,
+
/**
* The signature returned by the exchange in a melt request was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -869,6 +991,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE = 1306,
+
/**
* The provided transfer keys do not match up with the original commitment. Information about the original commitment is included in the response.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -876,6 +999,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_COMMITMENT_VIOLATION = 1353,
+
/**
* Failed to produce the blinded signatures over the coins to be returned.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -883,6 +1007,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_SIGNING_ERROR = 1354,
+
/**
* The exchange is unaware of the refresh session specified in the request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -890,6 +1015,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN = 1355,
+
/**
* The size of the cut-and-choose dimension of the private transfer keys request does not match #TALER_CNC_KAPPA - 1.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -897,6 +1023,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID = 1356,
+
/**
* The number of envelopes given does not match the number of denomination keys given.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -904,6 +1031,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_NEW_DENOMS_ARRAY_SIZE_MISMATCH = 1358,
+
/**
* The exchange encountered a numeric overflow totaling up the cost for the refresh operation.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -911,6 +1039,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW = 1359,
+
/**
* The exchange's cost calculation shows that the melt amount is below the costs of the transaction.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -918,6 +1047,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_AMOUNT_INSUFFICIENT = 1360,
+
/**
* The signature made with the coin over the link data is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -925,6 +1055,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_LINK_SIGNATURE_INVALID = 1361,
+
/**
* The refresh session hash given to a /refreshes/ handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -932,6 +1063,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_INVALID_RCH = 1362,
+
/**
* Operation specified invalid for this endpoint.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -939,6 +1071,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_OPERATION_INVALID = 1363,
+
/**
* The client provided age commitment data, but age restriction is not supported on this server.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -946,6 +1079,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_NOT_SUPPORTED = 1364,
+
/**
* The client provided invalid age commitment data: missing, not an array, or array of invalid size.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -953,6 +1087,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID = 1365,
+
/**
* The coin specified in the link request is unknown to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -960,6 +1095,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_LINK_COIN_UNKNOWN = 1400,
+
/**
* The public key of given to a /transfers/ handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -967,6 +1103,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WTID_MALFORMED = 1450,
+
/**
* The exchange did not find information about the specified wire transfer identifier in the database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -974,6 +1111,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WTID_NOT_FOUND = 1451,
+
/**
* The exchange did not find information about the wire transfer fees it charged.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -981,6 +1119,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WIRE_FEE_NOT_FOUND = 1452,
+
/**
* The exchange found a wire fee that was above the total transfer value (and thus could not have been charged).
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -988,6 +1127,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT = 1453,
+
/**
* The wait target of the URL was not in the set of expected values.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -995,6 +1135,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSES_INVALID_WAIT_TARGET = 1475,
+
/**
* The signature on the purse status returned by the exchange was invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1002,6 +1143,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSES_GET_INVALID_SIGNATURE_BY_EXCHANGE = 1476,
+
/**
* The exchange knows literally nothing about the coin we were asked to refund. But without a transaction history, we cannot issue a refund. This is kind-of OK, the owner should just refresh it directly without executing the refund.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1009,6 +1151,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_COIN_NOT_FOUND = 1500,
+
/**
* We could not process the refund request as the coin's transaction history does not permit the requested refund because then refunds would exceed the deposit amount. The "history" in the response proves this.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1016,6 +1159,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT = 1501,
+
/**
* The exchange knows about the coin we were asked to refund, but not about the specific /deposit operation. Hence, we cannot issue a refund (as we do not know if this merchant public key is authorized to do a refund).
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1023,6 +1167,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_DEPOSIT_NOT_FOUND = 1502,
+
/**
* The exchange can no longer refund the customer/coin as the money was already transferred (paid out) to the merchant. (It should be past the refund deadline.)
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -1030,6 +1175,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_MERCHANT_ALREADY_PAID = 1503,
+
/**
* The refund fee specified for the request is lower than the refund fee charged by the exchange for the given denomination key of the refunded coin.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1037,6 +1183,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_FEE_TOO_LOW = 1504,
+
/**
* The refunded amount is smaller than the refund fee, which would result in a negative refund.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1044,6 +1191,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_FEE_ABOVE_AMOUNT = 1505,
+
/**
* The signature of the merchant is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1051,6 +1199,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID = 1506,
+
/**
* Merchant backend failed to create the refund confirmation signature.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1058,6 +1207,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_MERCHANT_SIGNING_FAILED = 1507,
+
/**
* The signature returned by the exchange in a refund request was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1065,6 +1215,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE = 1508,
+
/**
* The failure proof returned by the exchange is incorrect.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1072,6 +1223,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE = 1509,
+
/**
* Conflicting refund granted before with different amount but same refund transaction ID.
* Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
@@ -1079,6 +1231,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_INCONSISTENT_AMOUNT = 1510,
+
/**
* The given coin signature is invalid for the request.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1086,6 +1239,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_SIGNATURE_INVALID = 1550,
+
/**
* The exchange could not find the corresponding withdraw operation. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1093,6 +1247,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND = 1551,
+
/**
* The coin's remaining balance is zero. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1100,6 +1255,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_COIN_BALANCE_ZERO = 1552,
+
/**
* The exchange failed to reproduce the coin's blinding.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1107,6 +1263,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_BLINDING_FAILED = 1553,
+
/**
* The coin's remaining balance is zero. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1114,6 +1271,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_COIN_BALANCE_NEGATIVE = 1554,
+
/**
* The coin's denomination has not been revoked yet.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1121,6 +1279,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_NOT_ELIGIBLE = 1555,
+
/**
* The given coin signature is invalid for the request.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1128,6 +1287,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID = 1575,
+
/**
* The exchange could not find the corresponding melt operation. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1135,6 +1295,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND = 1576,
+
/**
* The exchange failed to reproduce the coin's blinding.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1142,6 +1303,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED = 1578,
+
/**
* The coin's denomination has not been revoked yet.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1149,6 +1311,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE = 1580,
+
/**
* This exchange does not allow clients to request /keys for times other than the current (exchange) time.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1156,6 +1319,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KEYS_TIMETRAVEL_FORBIDDEN = 1600,
+
/**
* A signature in the server's response was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1163,6 +1327,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_SIGNATURE_INVALID = 1650,
+
/**
* No bank accounts are enabled for the exchange. The administrator should enable-account using the taler-exchange-offline tool.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1170,6 +1335,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED = 1651,
+
/**
* The payto:// URI stored in the exchange database for its bank account is malformed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1177,6 +1343,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED = 1652,
+
/**
* No wire fees are configured for an enabled wire method of the exchange. The administrator must set the wire-fee using the taler-exchange-offline tool.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1184,6 +1351,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_FEES_NOT_CONFIGURED = 1653,
+
/**
* This purse was previously created with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1191,6 +1359,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA = 1675,
+
/**
* This purse was previously merged with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1198,6 +1367,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA = 1676,
+
/**
* The reserve has insufficient funds to create another purse.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1205,6 +1375,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS = 1677,
+
/**
* The purse fee specified for the request is lower than the purse fee charged by the exchange at this time.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1212,6 +1383,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW = 1678,
+
/**
* The payment request cannot be deleted anymore, as it either already completed or timed out.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1219,6 +1391,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DELETE_ALREADY_DECIDED = 1679,
+
/**
* The signature affirming the purse deletion is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1226,6 +1399,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DELETE_SIGNATURE_INVALID = 1680,
+
/**
* Withdrawal from the reserve requires age restriction to be set.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1233,6 +1407,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED = 1681,
+
/**
* The exchange failed to talk to the process responsible for its private denomination keys or the helpers had no denominations (properly) configured.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1240,6 +1415,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DENOMINATION_HELPER_UNAVAILABLE = 1700,
+
/**
* The response from the denomination key helper process was malformed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1247,6 +1423,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DENOMINATION_HELPER_BUG = 1701,
+
/**
* The helper refuses to sign with the key, because it is too early: the validity period has not yet started.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1254,6 +1431,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DENOMINATION_HELPER_TOO_EARLY = 1702,
+
/**
* The signature of the exchange on the reply was invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1261,6 +1439,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID = 1725,
+
/**
* The exchange failed to talk to the process responsible for its private signing keys.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1268,6 +1447,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE = 1750,
+
/**
* The response from the online signing key helper process was malformed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1275,6 +1455,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_SIGNKEY_HELPER_BUG = 1751,
+
/**
* The helper refuses to sign with the key, because it is too early: the validity period has not yet started.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1282,6 +1463,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_SIGNKEY_HELPER_TOO_EARLY = 1752,
+
/**
* The purse expiration time is in the past at the time of its creation.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1289,6 +1471,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW = 1775,
+
/**
* The purse expiration time is set to never, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1296,6 +1479,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER = 1776,
+
/**
* The signature affirming the merge of the purse is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1303,6 +1487,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID = 1777,
+
/**
* The signature by the reserve affirming the merge is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1310,6 +1495,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID = 1778,
+
/**
* The signature by the reserve affirming the open operation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1317,6 +1503,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE = 1785,
+
/**
* The signature by the reserve affirming the close operation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1324,6 +1511,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE = 1786,
+
/**
* The signature by the reserve affirming the attestion request is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1331,6 +1519,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE = 1787,
+
/**
* The exchange does not know an origin account to which the remaining reserve balance could be wired to, and the wallet failed to provide one.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1338,6 +1527,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT = 1788,
+
/**
* The reserve balance is insufficient to pay for the open operation.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1345,6 +1535,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS = 1789,
+
/**
* The auditor that was supposed to be disabled is unknown to this exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1352,6 +1543,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_NOT_FOUND = 1800,
+
/**
* The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1359,6 +1551,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT = 1801,
+
/**
* The signature to add or enable the auditor does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1366,6 +1559,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_ADD_SIGNATURE_INVALID = 1802,
+
/**
* The signature to disable the auditor does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1373,6 +1567,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_DEL_SIGNATURE_INVALID = 1803,
+
/**
* The signature to revoke the denomination does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1380,6 +1575,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_DENOMINATION_REVOKE_SIGNATURE_INVALID = 1804,
+
/**
* The signature to revoke the online signing key does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1387,6 +1583,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_SIGNKEY_REVOKE_SIGNATURE_INVALID = 1805,
+
/**
* The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1394,6 +1591,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT = 1806,
+
/**
* The signingkey specified is unknown to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1401,6 +1599,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN = 1807,
+
/**
* The signature to publish wire account does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1408,6 +1607,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_DETAILS_SIGNATURE_INVALID = 1808,
+
/**
* The signature to add the wire account does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1415,6 +1615,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_ADD_SIGNATURE_INVALID = 1809,
+
/**
* The signature to disable the wire account does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1422,6 +1623,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_DEL_SIGNATURE_INVALID = 1810,
+
/**
* The wire account to be disabled is unknown to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1429,6 +1631,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_NOT_FOUND = 1811,
+
/**
* The signature to affirm wire fees does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1436,6 +1639,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_FEE_SIGNATURE_INVALID = 1812,
+
/**
* The signature conflicts with a previous signature affirming different fees.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1443,6 +1647,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_FEE_MISMATCH = 1813,
+
/**
* The signature affirming the denomination key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1450,6 +1655,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID = 1814,
+
/**
* The signature affirming the signing key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1457,6 +1663,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID = 1815,
+
/**
* The signature conflicts with a previous signature affirming different fees.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1464,6 +1671,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH = 1816,
+
/**
* The signature affirming the fee structure is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1471,6 +1679,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID = 1817,
+
/**
* The signature affirming the profit drain is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1478,6 +1687,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_DRAIN_PROFITS_SIGNATURE_INVALID = 1818,
+
/**
* The signature affirming the AML decision is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1485,6 +1695,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AML_DECISION_ADD_SIGNATURE_INVALID = 1825,
+
/**
* The AML officer specified is not allowed to make AML decisions right now.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1492,6 +1703,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AML_DECISION_INVALID_OFFICER = 1826,
+
/**
* There is a more recent AML decision on file. The decision was rejected as timestamps of AML decisions must be monotonically increasing.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1499,6 +1711,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT = 1827,
+
/**
* There AML decision would impose an AML check of a type that is not provided by any KYC provider known to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1506,6 +1719,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AML_DECISION_UNKNOWN_CHECK = 1828,
+
/**
* The signature affirming the change in the AML officer status is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1513,6 +1727,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_UPDATE_AML_OFFICER_SIGNATURE_INVALID = 1830,
+
/**
* A more recent decision about the AML officer status is known to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1520,6 +1735,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AML_OFFICERS_MORE_RECENT_PRESENT = 1831,
+
/**
* The purse was previously created with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1527,6 +1743,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA = 1850,
+
/**
* The purse was previously created with a different contract.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1534,6 +1751,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_CONFLICTING_CONTRACT_STORED = 1851,
+
/**
* A coin signature for a deposit into the purse is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1541,6 +1759,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID = 1852,
+
/**
* The purse expiration time is in the past.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1548,6 +1767,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_EXPIRATION_BEFORE_NOW = 1853,
+
/**
* The purse expiration time is "never".
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1555,6 +1775,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER = 1854,
+
/**
* The purse signature over the purse meta data is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1562,6 +1783,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID = 1855,
+
/**
* The signature over the encrypted contract is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1569,6 +1791,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID = 1856,
+
/**
* The signature from the exchange over the confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1576,6 +1799,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID = 1857,
+
/**
* The coin was previously deposited with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1583,6 +1807,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA = 1858,
+
/**
* The encrypted contract was previously uploaded with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1590,6 +1815,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA = 1859,
+
/**
* The deposited amount is less than the purse fee.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1597,6 +1823,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE = 1860,
+
/**
* The signature using the merge key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1604,6 +1831,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE = 1876,
+
/**
* The signature using the reserve key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1611,6 +1839,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE = 1877,
+
/**
* The targeted purse is not yet full and thus cannot be merged. Retrying the request later may succeed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1618,6 +1847,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_NOT_FULL = 1878,
+
/**
* The signature from the exchange over the confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1625,6 +1855,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID = 1879,
+
/**
* The exchange of the target account is not a partner of this exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1632,6 +1863,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN = 1880,
+
/**
* The signature affirming the new partner is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1639,6 +1871,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_ADD_PARTNER_SIGNATURE_INVALID = 1890,
+
/**
* Conflicting data for the partner already exists with the exchange.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1646,6 +1879,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT = 1891,
+
/**
* The auditor signature over the denomination meta data is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1653,6 +1887,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AUDITORS_AUDITOR_SIGNATURE_INVALID = 1900,
+
/**
* The auditor that was specified is unknown to this exchange.
* Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
@@ -1660,6 +1895,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AUDITORS_AUDITOR_UNKNOWN = 1901,
+
/**
* The auditor that was specified is no longer used by this exchange.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -1667,6 +1903,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AUDITORS_AUDITOR_INACTIVE = 1902,
+
/**
* The signature affirming the wallet's KYC request was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1674,6 +1911,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_WALLET_SIGNATURE_INVALID = 1925,
+
/**
* The exchange received an unexpected malformed response from its KYC backend.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1681,6 +1919,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE = 1926,
+
/**
* The backend signaled an unexpected failure.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1688,6 +1927,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_BACKEND_ERROR = 1927,
+
/**
* The backend signaled an authorization failure.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1695,6 +1935,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_BACKEND_AUTHORIZATION_FAILED = 1928,
+
/**
* The exchange is unaware of having made an the authorization request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1702,6 +1943,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN = 1929,
+
/**
* The payto-URI hash did not match. Hence the request was denied.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1709,6 +1951,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED = 1930,
+
/**
* The request used a logic specifier that is not known to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1716,6 +1959,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN = 1931,
+
/**
* The request requires a logic which is no longer configured at the exchange.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1723,6 +1967,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_LOGIC_GONE = 1932,
+
/**
* The logic plugin had a bug in its interaction with the KYC provider.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1730,6 +1975,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_LOGIC_BUG = 1933,
+
/**
* The exchange could not process the request with its KYC provider because the provider refused access to the service. This indicates some configuration issue at the Taler exchange operator.
* Returned with an HTTP status code of #MHD_HTTP_NETWORK_AUTHENTICATION_REQUIRED (511).
@@ -1737,6 +1983,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED = 1934,
+
/**
* There was a timeout in the interaction between the exchange and the KYC provider. The most likely cause is some networking problem. Trying again later might succeed.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -1744,6 +1991,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT = 1935,
+
/**
* The KYC provider responded with a status that was completely unexpected by the KYC logic of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1751,6 +1999,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY = 1936,
+
/**
* The rate limit of the exchange at the KYC provider has been exceeded. Trying much later might work.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
@@ -1758,6 +2007,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED = 1937,
+
/**
* The request to the webhook lacked proper authorization or authentication data.
* Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
@@ -1765,6 +2015,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_WEBHOOK_UNAUTHORIZED = 1938,
+
/**
* The exchange does not know a contract under the given contract public key.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1772,6 +2023,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_UNKNOWN = 1950,
+
/**
* The URL does not encode a valid exchange public key in its path.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1779,6 +2031,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_INVALID_CONTRACT_PUB = 1951,
+
/**
* The returned encrypted contract did not decrypt.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1786,6 +2039,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_DECRYPTION_FAILED = 1952,
+
/**
* The signature on the encrypted contract did not validate.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1793,6 +2047,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_SIGNATURE_INVALID = 1953,
+
/**
* The decrypted contract was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1800,6 +2055,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_DECODING_FAILED = 1954,
+
/**
* A coin signature for a deposit into the purse is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1807,6 +2063,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_COIN_SIGNATURE_INVALID = 1975,
+
/**
* It is too late to deposit coins into the purse.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -1814,6 +2071,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY = 1976,
+
/**
* TOTP key is not valid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1821,6 +2079,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TOTP_KEY_INVALID = 1980,
+
/**
* The backend could not find the merchant instance specified in the request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1828,6 +2087,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_INSTANCE_UNKNOWN = 2000,
+
/**
* The start and end-times in the wire fee structure leave a hole. This is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1835,6 +2095,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE = 2001,
+
/**
* The merchant was unable to obtain a valid answer to /wire from the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1842,6 +2103,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_WIRE_REQUEST_FAILED = 2002,
+
/**
* The proposal is not known to the backend.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1849,6 +2111,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_ORDER_UNKNOWN = 2005,
+
/**
* The order provided to the backend could not be completed, because a product to be completed via inventory data is not actually in our inventory.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1856,6 +2119,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_PRODUCT_UNKNOWN = 2006,
+
/**
* The reward ID is unknown. This could happen if the reward has expired.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1863,6 +2127,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_REWARD_ID_UNKNOWN = 2007,
+
/**
* The contract obtained from the merchant backend was malformed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1870,6 +2135,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID = 2008,
+
/**
* The order we found does not match the provided contract hash.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1877,6 +2143,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER = 2009,
+
/**
* The exchange failed to provide a valid response to the merchant's /keys request.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1884,6 +2151,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE = 2010,
+
/**
* The exchange failed to respond to the merchant on time.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -1891,6 +2159,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_TIMEOUT = 2011,
+
/**
* The merchant failed to talk to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1898,6 +2167,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE = 2012,
+
/**
* The exchange returned a maformed response.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1905,6 +2175,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED = 2013,
+
/**
* The exchange returned an unexpected response status.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1912,6 +2183,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS = 2014,
+
/**
* The merchant refused the request due to lack of authorization.
* Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
@@ -1919,6 +2191,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_UNAUTHORIZED = 2015,
+
/**
* The merchant instance specified in the request was deleted.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1926,6 +2199,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_INSTANCE_DELETED = 2016,
+
/**
* The backend could not find the inbound wire transfer specified in the request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1933,6 +2207,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_TRANSFER_UNKNOWN = 2017,
+
/**
* The backend could not find the template(id) because it is not exist.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1940,6 +2215,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_TEMPLATE_UNKNOWN = 2018,
+
/**
* The backend could not find the webhook(id) because it is not exist.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1947,6 +2223,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_WEBHOOK_UNKNOWN = 2019,
+
/**
* The backend could not find the webhook(serial) because it is not exist.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1954,6 +2231,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_PENDING_WEBHOOK_UNKNOWN = 2020,
+
/**
* The backend could not find the OTP device(id) because it is not exist.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1961,6 +2239,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN = 2021,
+
/**
* The account is not known to the backend.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1968,6 +2247,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_ACCOUNT_UNKNOWN = 2022,
+
/**
* The wire hash was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1975,6 +2255,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_H_WIRE_MALFORMED = 2023,
+
/**
* The currency specified in the operation does not work with the current state of the given resource.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1982,6 +2263,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_CURRENCY_MISMATCH = 2024,
+
/**
* The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_OK (200).
@@ -1989,6 +2271,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE = 2100,
+
/**
* The merchant backend failed to construct the request for tracking to the exchange, thus tracking details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1996,6 +2279,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_EXCHANGE_REQUEST_FAILURE = 2103,
+
/**
* The merchant backend failed trying to contact the exchange for tracking details, thus those details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2003,6 +2287,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE = 2104,
+
/**
* The claim token used to authenticate the client is invalid for this order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2010,6 +2295,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_INVALID_TOKEN = 2105,
+
/**
* The contract terms hash used to authenticate the client is invalid for this order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2017,6 +2303,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH = 2106,
+
/**
* The exchange responded saying that funds were insufficient (for example, due to double-spending).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2024,6 +2311,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS = 2150,
+
/**
* The denomination key used for payment is not listed among the denomination keys of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2031,6 +2319,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND = 2151,
+
/**
* The denomination key used for payment is not audited by an auditor approved by the merchant.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2038,6 +2327,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_AUDITOR_FAILURE = 2152,
+
/**
* There was an integer overflow totaling up the amounts or deposit fees in the payment.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2045,6 +2335,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW = 2153,
+
/**
* The deposit fees exceed the total value of the payment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2052,20 +2343,23 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT = 2154,
+
/**
* After considering deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. The client should revisit the logic used to calculate fees it must cover.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES = 2155,
+
/**
* Even if we do not consider deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT = 2156,
+
/**
* The signature over the contract of one of the coins was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2073,6 +2367,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_COIN_SIGNATURE_INVALID = 2157,
+
/**
* When we tried to find information about the exchange to issue the deposit, we failed. This usually only happens if the merchant backend is somehow unable to get its own HTTP client logic to work.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2080,6 +2375,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED = 2158,
+
/**
* The refund deadline in the contract is after the transfer deadline.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2087,6 +2383,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE = 2159,
+
/**
* The order was already paid (maybe by another wallet).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2094,6 +2391,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID = 2160,
+
/**
* The payment is too late, the offer has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2101,6 +2399,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED = 2161,
+
/**
* The "merchant" field is missing in the proposal data. This is an internal error as the proposal is from the merchant's own database at this point.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2108,6 +2407,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING = 2162,
+
/**
* Failed to locate merchant's account information matching the wire hash given in the proposal.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2115,6 +2415,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN = 2163,
+
/**
* The deposit time for the denomination has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2122,6 +2423,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED = 2165,
+
/**
* The exchange of the deposited coin charges a wire fee that could not be added to the total (total amount too high).
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2129,6 +2431,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED = 2166,
+
/**
* The contract was not fully paid because of refunds. Note that clients MAY treat this as paid if, for example, contracts must be executed despite of refunds.
* Returned with an HTTP status code of #MHD_HTTP_PAYMENT_REQUIRED (402).
@@ -2136,6 +2439,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_REFUNDED = 2167,
+
/**
* According to our database, we have refunded more than we were paid (which should not be possible).
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2143,6 +2447,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS = 2168,
+
/**
* Legacy stuff. Remove me with protocol v1.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2150,6 +2455,7 @@ export enum TalerErrorCode {
*/
DEAD_QQQ_PAY_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE = 2169,
+
/**
* The payment failed at the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2157,6 +2463,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_FAILED = 2170,
+
/**
* The payment required a minimum age but one of the coins (of a denomination with support for age restriction) did not provide any age_commitment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2164,6 +2471,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING = 2171,
+
/**
* The payment required a minimum age but one of the coins provided an age_commitment that contained a wrong number of public keys compared to the number of age groups defined in the denomination of the coin.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2171,6 +2479,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH = 2172,
+
/**
* The payment required a minimum age but one of the coins provided a minimum_age_sig that couldn't be verified with the given age_commitment for that particular minimum age.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2178,6 +2487,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED = 2173,
+
/**
* The payment required no minimum age but one of the coins (of a denomination with support for age restriction) did not provide the required h_age_commitment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2185,6 +2495,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING = 2174,
+
/**
* The exchange does not support the selected bank account of the merchant. Likely the merchant had stale data on the bank accounts of the exchange and thus selected an inappropriate exchange when making the offer.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2192,6 +2503,63 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED = 2175,
+
+ /**
+ * The payment requires the wallet to select a choice from the choices array and pass it in the 'choice_index' field of the request.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING = 2176,
+
+
+ /**
+ * The 'choice_index' field is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS = 2177,
+
+
+ /**
+ * The provided 'tokens' array does not match with the required input tokens of the order.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_INPUT_TOKENS_MISMATCH = 2178,
+
+
+ /**
+ * Invalid token issue signature (blindly signed by merchant) for provided token.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ISSUE_SIG_INVALID = 2179,
+
+
+ /**
+ * Invalid token use signature (EdDSA, signed by wallet) for provided token.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_USE_SIG_INVALID = 2180,
+
+
+ /**
+ * The provided number of tokens does not match the required number.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_COUNT_MISMATCH = 2181,
+
+
+ /**
+ * The provided number of token envelopes does not match the specified number.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ENVELOPE_COUNT_MISMATCH = 2182,
+
+
/**
* The contract hash does not match the given order ID.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2199,6 +2567,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAID_CONTRACT_HASH_MISMATCH = 2200,
+
/**
* The signature of the merchant is not valid for the given contract hash.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2206,6 +2575,23 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAID_COIN_SIGNATURE_INVALID = 2201,
+
+ /**
+ * A token family with this ID but conflicting data exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_TOKEN_FAMILY_CONFLICT = 2225,
+
+
+ /**
+ * The backend is unaware of a token family with the given ID.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PATCH_TOKEN_FAMILY_NOT_FOUND = 2226,
+
+
/**
* The merchant failed to send the exchange the refund request.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2213,6 +2599,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED = 2251,
+
/**
* The merchant failed to find the exchange to process the lookup.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2220,6 +2607,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_LOOKUP_FAILED = 2252,
+
/**
* The merchant could not find the contract.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2227,6 +2615,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND = 2253,
+
/**
* The payment was already completed and thus cannot be aborted anymore.
* Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
@@ -2234,6 +2623,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE = 2254,
+
/**
* The hash provided by the wallet does not match the order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2241,6 +2631,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH = 2255,
+
/**
* The array of coins cannot be empty.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2248,6 +2639,63 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY = 2256,
+
+ /**
+ * We are waiting for the exchange to provide us with key material before checking the wire transfer.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_AWAITING_KEYS = 2258,
+
+
+ /**
+ * We are waiting for the exchange to provide us with the list of aggregated transactions.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_AWAITING_LIST = 2259,
+
+
+ /**
+ * The endpoint indicated in the wire transfer does not belong to a GNU Taler exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_FATAL_NO_EXCHANGE = 2260,
+
+
+ /**
+ * The exchange indicated in the wire transfer claims to know nothing about the wire transfer.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_FATAL_NOT_FOUND = 2261,
+
+
+ /**
+ * The interaction with the exchange is delayed due to rate limiting.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_RATE_LIMITED = 2262,
+
+
+ /**
+ * We experienced a transient failure in our interaction with the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE = 2263,
+
+
+ /**
+ * The response from the exchange was unacceptable and should be reviewed with an auditor.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_HARD_FAILURE = 2264,
+
+
/**
* We could not claim the order because the backend is unaware of it.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2255,6 +2703,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND = 2300,
+
/**
* We could not claim the order because someone else claimed it first.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2262,6 +2711,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED = 2301,
+
/**
* The client-side experienced an internal failure.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2269,6 +2719,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_CLAIM_CLIENT_INTERNAL_FAILURE = 2302,
+
/**
* The backend failed to sign the refund request.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2276,6 +2727,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_REFUND_SIGNATURE_FAILED = 2350,
+
/**
* The client failed to unblind the signature returned by the merchant.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2283,6 +2735,7 @@ export enum TalerErrorCode {
*/
MERCHANT_REWARD_PICKUP_UNBLIND_FAILURE = 2400,
+
/**
* The exchange returned a failure code for the withdraw operation.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2290,6 +2743,7 @@ export enum TalerErrorCode {
*/
MERCHANT_REWARD_PICKUP_EXCHANGE_ERROR = 2403,
+
/**
* The merchant failed to add up the amounts to compute the pick up value.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2297,6 +2751,7 @@ export enum TalerErrorCode {
*/
MERCHANT_REWARD_PICKUP_SUMMATION_FAILED = 2404,
+
/**
* The reward expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2304,6 +2759,7 @@ export enum TalerErrorCode {
*/
MERCHANT_REWARD_PICKUP_HAS_EXPIRED = 2405,
+
/**
* The requested withdraw amount exceeds the amount remaining to be picked up.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2311,6 +2767,7 @@ export enum TalerErrorCode {
*/
MERCHANT_REWARD_PICKUP_AMOUNT_EXCEEDS_REWARD_REMAINING = 2406,
+
/**
* The merchant did not find the specified denomination key in the exchange's key set.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2318,6 +2775,7 @@ export enum TalerErrorCode {
*/
MERCHANT_REWARD_PICKUP_DENOMINATION_UNKNOWN = 2407,
+
/**
* The merchant instance has no active bank accounts configured. However, at least one bank account must be available to create new orders.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2325,6 +2783,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE = 2500,
+
/**
* The proposal had no timestamp and the merchant backend failed to obtain the current local time.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2332,6 +2791,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME = 2501,
+
/**
* The order provided to the backend could not be parsed; likely some required fields were missing or ill-formed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2339,6 +2799,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR = 2502,
+
/**
* A conflicting order (sharing the same order identifier) already exists at this merchant backend instance.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2346,6 +2807,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS = 2503,
+
/**
* The order creation request is invalid because the given wire deadline is before the refund deadline.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2353,6 +2815,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE = 2504,
+
/**
* The order creation request is invalid because the delivery date given is in the past.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2360,6 +2823,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST = 2505,
+
/**
* The order creation request is invalid because a wire deadline of "never" is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2367,6 +2831,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER = 2506,
+
/**
* The order creation request is invalid because the given payment deadline is in the past.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2374,6 +2839,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST = 2507,
+
/**
* The order creation request is invalid because the given refund deadline is in the past.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2381,6 +2847,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST = 2508,
+
/**
* The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2388,6 +2855,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD = 2509,
+
/**
* One of the paths to forget is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2395,6 +2863,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT = 2510,
+
/**
* One of the paths to forget was not marked as forgettable.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2402,6 +2871,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_NOT_FORGETTABLE = 2511,
+
/**
* The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2409,6 +2879,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT = 2520,
+
/**
* The order provided to the backend could not be deleted as the order was already paid.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2416,6 +2887,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID = 2521,
+
/**
* The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it exceeds the original price paid by the customer.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2423,6 +2895,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_INCONSISTENT_AMOUNT = 2530,
+
/**
* Only paid orders can be refunded, and the frontend specified an unpaid order to issue a refund for.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2430,6 +2903,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID = 2531,
+
/**
* The refund delay was set to 0 and thus no refunds are ever allowed for this order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2437,6 +2911,15 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT = 2532,
+
+ /**
+ * The token family slug provided in this order could not be found in the merchant database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN = 2533,
+
+
/**
* The exchange says it does not know this transfer.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2444,6 +2927,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_EXCHANGE_UNKNOWN = 2550,
+
/**
* We internally failed to execute the /track/transfer request.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2451,6 +2935,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_REQUEST_ERROR = 2551,
+
/**
* The amount transferred differs between what was submitted and what the exchange claimed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2458,6 +2943,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_TRANSFERS = 2552,
+
/**
* The exchange gave conflicting information about a coin which has been wire transferred.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2465,6 +2951,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS = 2553,
+
/**
* The exchange charged a different wire fee than what it originally advertised, and it is higher.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2472,6 +2959,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE = 2554,
+
/**
* We did not find the account that the transfer was made to.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2479,6 +2967,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_ACCOUNT_NOT_FOUND = 2555,
+
/**
* The backend could not delete the transfer as the echange already replied to our inquiry about it and we have integrated the result.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2486,6 +2975,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_TRANSFERS_ALREADY_CONFIRMED = 2556,
+
/**
* The backend was previously informed about a wire transfer with the same ID but a different amount. Multiple wire transfers with the same ID are not allowed. If the new amount is correct, the old transfer should first be deleted.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2493,54 +2983,6 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION = 2557,
- /**
- * We are waiting for the exchange to provide us with key material before checking the wire transfer.
- * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
- * (A value of 0 indicates that the error is generated client-side).
- */
- MERCHANT_EXCHANGE_TRANSFERS_AWAITING_KEYS = 2258,
-
- /**
- * We are waiting for the exchange to provide us with the list of aggregated transactions.
- * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
- * (A value of 0 indicates that the error is generated client-side).
- */
- MERCHANT_EXCHANGE_TRANSFERS_AWAITING_LIST = 2259,
-
- /**
- * The endpoint indicated in the wire transfer does not belong to a GNU Taler exchange.
- * Returned with an HTTP status code of #MHD_HTTP_OK (200).
- * (A value of 0 indicates that the error is generated client-side).
- */
- MERCHANT_EXCHANGE_TRANSFERS_FATAL_NO_EXCHANGE = 2260,
-
- /**
- * The exchange indicated in the wire transfer claims to know nothing about the wire transfer.
- * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
- * (A value of 0 indicates that the error is generated client-side).
- */
- MERCHANT_EXCHANGE_TRANSFERS_FATAL_NOT_FOUND = 2261,
-
- /**
- * The interaction with the exchange is delayed due to rate limiting.
- * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
- * (A value of 0 indicates that the error is generated client-side).
- */
- MERCHANT_EXCHANGE_TRANSFERS_RATE_LIMITED = 2262,
-
- /**
- * We experienced a transient failure in our interaction with the exchange.
- * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
- * (A value of 0 indicates that the error is generated client-side).
- */
- MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE = 2263,
-
- /**
- * The response from the exchange was unacceptable and should be reviewed with an auditor.
- * Returned with an HTTP status code of #MHD_HTTP_OK (200).
- * (A value of 0 indicates that the error is generated client-side).
- */
- MERCHANT_EXCHANGE_TRANSFERS_HARD_FAILURE = 2264,
/**
* The amount transferred differs between what was submitted and what the exchange claimed.
@@ -2549,6 +2991,7 @@ export enum TalerErrorCode {
*/
MERCHANT_EXCHANGE_TRANSFERS_CONFLICTING_TRANSFERS = 2563,
+
/**
* The merchant backend cannot create an instance under the given identifier as one already exists. Use PATCH to modify the existing entry.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2556,6 +2999,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS = 2600,
+
/**
* The merchant backend cannot create an instance because the authentication configuration field is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2563,6 +3007,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCES_BAD_AUTH = 2601,
+
/**
* The merchant backend cannot update an instance's authentication settings because the provided authentication settings are malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2570,6 +3015,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH = 2602,
+
/**
* The merchant backend cannot create an instance under the given identifier, the previous one was deleted but must be purged first.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2577,6 +3023,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED = 2603,
+
/**
* The merchant backend cannot update an instance under the given identifier, the previous one was deleted but must be purged first.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2584,6 +3031,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED = 2625,
+
/**
* The bank account referenced in the requested operation was not found.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2591,6 +3039,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_ACCOUNT_DELETE_UNKNOWN_ACCOUNT = 2626,
+
/**
* The bank account specified in the request already exists at the merchant.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2598,6 +3047,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_ACCOUNT_EXISTS = 2627,
+
/**
* The product ID exists.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2605,6 +3055,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS = 2650,
+
/**
* The update would have reduced the total amount of product lost, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2612,6 +3063,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED = 2660,
+
/**
* The update would have mean that more stocks were lost than what remains from total inventory after sales, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2619,6 +3071,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS = 2661,
+
/**
* The update would have reduced the total amount of product in stock, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2626,6 +3079,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED = 2662,
+
/**
* The update would have reduced the total amount of product sold, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2633,6 +3087,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED = 2663,
+
/**
* The lock request is for more products than we have left (unlocked) in stock.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2640,6 +3095,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS = 2670,
+
/**
* The deletion request is for a product that is locked.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2647,6 +3103,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_PRODUCTS_CONFLICTING_LOCK = 2680,
+
/**
* The requested wire method is not supported by the exchange.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2654,6 +3111,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_RESERVES_UNSUPPORTED_WIRE_METHOD = 2700,
+
/**
* The requested exchange does not allow rewards.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2661,6 +3119,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_RESERVES_REWARDS_NOT_ALLOWED = 2701,
+
/**
* The reserve could not be deleted because it is unknown.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2668,6 +3127,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_RESERVES_NO_SUCH_RESERVE = 2710,
+
/**
* The reserve that was used to fund the rewards has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2675,6 +3135,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_EXPIRED = 2750,
+
/**
* The reserve that was used to fund the rewards was not found in the DB.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
@@ -2682,6 +3143,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_UNKNOWN = 2751,
+
/**
* The backend knows the instance that was supposed to support the reward, and it was configured for rewardping. However, the funds remaining are insufficient to cover the reward, and the merchant should top up the reserve.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2689,6 +3151,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_INSUFFICIENT_FUNDS = 2752,
+
/**
* The backend failed to find a reserve needed to authorize the reward.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
@@ -2696,6 +3159,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_NOT_FOUND = 2753,
+
/**
* The merchant backend encountered a failure in computing the deposit total.
* Returned with an HTTP status code of #MHD_HTTP_OK (200).
@@ -2703,6 +3167,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE = 2800,
+
/**
* The template ID already exists.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2710,6 +3175,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TEMPLATES_CONFLICT_TEMPLATE_EXISTS = 2850,
+
/**
* The OTP device ID already exists.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2717,6 +3183,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS = 2851,
+
/**
* Amount given in the using template and in the template contract. There is a conflict.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2724,6 +3191,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT = 2860,
+
/**
* Subject given in the using template and in the template contract. There is a conflict.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2731,6 +3199,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT = 2861,
+
/**
* Amount not given in the using template and in the template contract. There is a conflict.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2738,6 +3207,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_USING_TEMPLATES_NO_AMOUNT = 2862,
+
/**
* Subject not given in the using template and in the template contract. There is a conflict.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2745,6 +3215,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY = 2863,
+
/**
* The webhook ID elready exists.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2752,6 +3223,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_WEBHOOKS_CONFLICT_WEBHOOK_EXISTS = 2900,
+
/**
* The webhook serial elready exists.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2759,6 +3231,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_PENDING_WEBHOOKS_CONFLICT_PENDING_WEBHOOK_EXISTS = 2910,
+
/**
* The signature from the exchange on the deposit confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2766,6 +3239,7 @@ export enum TalerErrorCode {
*/
AUDITOR_DEPOSIT_CONFIRMATION_SIGNATURE_INVALID = 3100,
+
/**
* The exchange key used for the signature on the deposit confirmation was revoked.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2773,6 +3247,23 @@ export enum TalerErrorCode {
*/
AUDITOR_EXCHANGE_SIGNING_KEY_REVOKED = 3101,
+
+ /**
+ * The requested resource could not be found.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_RESOURCE_NOT_FOUND = 3102,
+
+
+ /**
+ * The URI is missing a path component.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_URI_MISSING_PATH_COMPONENT = 3103,
+
+
/**
* Wire transfer attempted with credit and debit party being the same bank account.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2780,6 +3271,7 @@ export enum TalerErrorCode {
*/
BANK_SAME_ACCOUNT = 5101,
+
/**
* Wire transfer impossible, due to financial limitation of the party that attempted the payment.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2787,6 +3279,7 @@ export enum TalerErrorCode {
*/
BANK_UNALLOWED_DEBIT = 5102,
+
/**
* Negative numbers are not allowed (as value and/or fraction) to instantiate an amount object.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2794,6 +3287,7 @@ export enum TalerErrorCode {
*/
BANK_NEGATIVE_NUMBER_AMOUNT = 5103,
+
/**
* A too big number was used (as value and/or fraction) to instantiate an amount object.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2801,6 +3295,7 @@ export enum TalerErrorCode {
*/
BANK_NUMBER_TOO_BIG = 5104,
+
/**
* The bank account referenced in the requested operation was not found.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2808,6 +3303,7 @@ export enum TalerErrorCode {
*/
BANK_UNKNOWN_ACCOUNT = 5106,
+
/**
* The transaction referenced in the requested operation (typically a reject operation), was not found.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2815,6 +3311,7 @@ export enum TalerErrorCode {
*/
BANK_TRANSACTION_NOT_FOUND = 5107,
+
/**
* Bank received a malformed amount string.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2822,6 +3319,7 @@ export enum TalerErrorCode {
*/
BANK_BAD_FORMAT_AMOUNT = 5108,
+
/**
* The client does not own the account credited by the transaction which is to be rejected, so it has no rights do reject it.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2829,6 +3327,7 @@ export enum TalerErrorCode {
*/
BANK_REJECT_NO_RIGHTS = 5109,
+
/**
* This error code is returned when no known exception types captured the exception.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2836,6 +3335,7 @@ export enum TalerErrorCode {
*/
BANK_UNMANAGED_EXCEPTION = 5110,
+
/**
* This error code is used for all those exceptions that do not really need a specific error code to return to the client. Used for example when a client is trying to register with a unavailable username.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2843,6 +3343,7 @@ export enum TalerErrorCode {
*/
BANK_SOFT_EXCEPTION = 5111,
+
/**
* The request UID for a request to transfer funds has already been used, but with different details for the transfer.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2850,6 +3351,7 @@ export enum TalerErrorCode {
*/
BANK_TRANSFER_REQUEST_UID_REUSED = 5112,
+
/**
* The withdrawal operation already has a reserve selected. The current request conflicts with the existing selection.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2857,6 +3359,7 @@ export enum TalerErrorCode {
*/
BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT = 5113,
+
/**
* The wire transfer subject duplicates an existing reserve public key. But wire transfer subjects must be unique.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2864,6 +3367,7 @@ export enum TalerErrorCode {
*/
BANK_DUPLICATE_RESERVE_PUB_SUBJECT = 5114,
+
/**
* The client requested a transaction that is so far in the past, that it has been forgotten by the bank.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2871,6 +3375,7 @@ export enum TalerErrorCode {
*/
BANK_ANCIENT_TRANSACTION_GONE = 5115,
+
/**
* The client attempted to abort a transaction that was already confirmed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2878,6 +3383,7 @@ export enum TalerErrorCode {
*/
BANK_ABORT_CONFIRM_CONFLICT = 5116,
+
/**
* The client attempted to confirm a transaction that was already aborted.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2885,6 +3391,7 @@ export enum TalerErrorCode {
*/
BANK_CONFIRM_ABORT_CONFLICT = 5117,
+
/**
* The client attempted to register an account with the same name.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2892,6 +3399,7 @@ export enum TalerErrorCode {
*/
BANK_REGISTER_CONFLICT = 5118,
+
/**
* The client attempted to confirm a withdrawal operation before the wallet posted the required details.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2899,6 +3407,7 @@ export enum TalerErrorCode {
*/
BANK_POST_WITHDRAWAL_OPERATION_REQUIRED = 5119,
+
/**
* The client tried to register a new account under a reserved username (like 'admin' for example).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2906,6 +3415,7 @@ export enum TalerErrorCode {
*/
BANK_RESERVED_USERNAME_CONFLICT = 5120,
+
/**
* The client tried to register a new account with an username already in use.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2913,6 +3423,7 @@ export enum TalerErrorCode {
*/
BANK_REGISTER_USERNAME_REUSE = 5121,
+
/**
* The client tried to register a new account with a payto:// URI already in use.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2920,6 +3431,7 @@ export enum TalerErrorCode {
*/
BANK_REGISTER_PAYTO_URI_REUSE = 5122,
+
/**
* The client tried to delete an account with a non null balance.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2927,6 +3439,7 @@ export enum TalerErrorCode {
*/
BANK_ACCOUNT_BALANCE_NOT_ZERO = 5123,
+
/**
* The client tried to create a transaction or an operation that credit an unknown account.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2934,6 +3447,7 @@ export enum TalerErrorCode {
*/
BANK_UNKNOWN_CREDITOR = 5124,
+
/**
* The client tried to create a transaction or an operation that debit an unknown account.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2941,6 +3455,7 @@ export enum TalerErrorCode {
*/
BANK_UNKNOWN_DEBTOR = 5125,
+
/**
* The client tried to perform an action prohibited for exchange accounts.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2948,6 +3463,7 @@ export enum TalerErrorCode {
*/
BANK_ACCOUNT_IS_EXCHANGE = 5126,
+
/**
* The client tried to perform an action reserved for exchange accounts.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2955,6 +3471,7 @@ export enum TalerErrorCode {
*/
BANK_ACCOUNT_IS_NOT_EXCHANGE = 5127,
+
/**
* Received currency conversion is wrong.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2962,6 +3479,7 @@ export enum TalerErrorCode {
*/
BANK_BAD_CONVERSION = 5128,
+
/**
* The account referenced in this operation is missing tan info for the chosen channel.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2969,6 +3487,7 @@ export enum TalerErrorCode {
*/
BANK_MISSING_TAN_INFO = 5129,
+
/**
* The client attempted to confirm a transaction with incomplete info.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2976,6 +3495,7 @@ export enum TalerErrorCode {
*/
BANK_CONFIRM_INCOMPLETE = 5130,
+
/**
* The request rate is too high. The server is refusing requests to guard against brute-force attacks.
* Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
@@ -2983,6 +3503,7 @@ export enum TalerErrorCode {
*/
BANK_TAN_RATE_LIMITED = 5131,
+
/**
* This TAN channel is not supported.
* Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
@@ -2990,6 +3511,7 @@ export enum TalerErrorCode {
*/
BANK_TAN_CHANNEL_NOT_SUPPORTED = 5132,
+
/**
* Failed to send TAN using the helper script. Either script is not found, or script timeout, or script terminated with a non-successful result.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2997,6 +3519,7 @@ export enum TalerErrorCode {
*/
BANK_TAN_CHANNEL_SCRIPT_FAILED = 5133,
+
/**
* The client's response to the challenge was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -3004,6 +3527,7 @@ export enum TalerErrorCode {
*/
BANK_TAN_CHALLENGE_FAILED = 5134,
+
/**
* A non-admin user has tried to change their legal name.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3011,6 +3535,7 @@ export enum TalerErrorCode {
*/
BANK_NON_ADMIN_PATCH_LEGAL_NAME = 5135,
+
/**
* A non-admin user has tried to change their debt limit.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3018,6 +3543,7 @@ export enum TalerErrorCode {
*/
BANK_NON_ADMIN_PATCH_DEBT_LIMIT = 5136,
+
/**
* A non-admin user has tried to change their password whihout providing the current one.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3025,6 +3551,7 @@ export enum TalerErrorCode {
*/
BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD = 5137,
+
/**
* Provided old password does not match current password.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3032,6 +3559,7 @@ export enum TalerErrorCode {
*/
BANK_PATCH_BAD_OLD_PASSWORD = 5138,
+
/**
* An admin user has tried to become an exchange.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3039,6 +3567,7 @@ export enum TalerErrorCode {
*/
BANK_PATCH_ADMIN_EXCHANGE = 5139,
+
/**
* A non-admin user has tried to change their cashout account.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3046,6 +3575,7 @@ export enum TalerErrorCode {
*/
BANK_NON_ADMIN_PATCH_CASHOUT = 5140,
+
/**
* A non-admin user has tried to change their contact info.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3053,6 +3583,7 @@ export enum TalerErrorCode {
*/
BANK_NON_ADMIN_PATCH_CONTACT = 5141,
+
/**
* The client tried to create a transaction that credit the admin account.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3060,6 +3591,7 @@ export enum TalerErrorCode {
*/
BANK_ADMIN_CREDITOR = 5142,
+
/**
* The referenced challenge was not found.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3067,6 +3599,7 @@ export enum TalerErrorCode {
*/
BANK_CHALLENGE_NOT_FOUND = 5143,
+
/**
* The referenced challenge has expired.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3074,6 +3607,7 @@ export enum TalerErrorCode {
*/
BANK_TAN_CHALLENGE_EXPIRED = 5144,
+
/**
* A non-admin user has tried to create an account with 2fa.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3081,6 +3615,23 @@ export enum TalerErrorCode {
*/
BANK_NON_ADMIN_SET_TAN_CHANNEL = 5145,
+
+ /**
+ * A non-admin user has tried to set their minimum cashout amount.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_SET_MIN_CASHOUT = 5146,
+
+
+ /**
+ * Amount of currency conversion it less than the minimum allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_CONVERSION_AMOUNT_TO_SMALL = 5147,
+
+
/**
* The sync service failed find the account in its database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3088,6 +3639,7 @@ export enum TalerErrorCode {
*/
SYNC_ACCOUNT_UNKNOWN = 6100,
+
/**
* The SHA-512 hash provided in the If-None-Match header is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3095,6 +3647,7 @@ export enum TalerErrorCode {
*/
SYNC_BAD_IF_NONE_MATCH = 6101,
+
/**
* The SHA-512 hash provided in the If-Match header is malformed or missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3102,6 +3655,7 @@ export enum TalerErrorCode {
*/
SYNC_BAD_IF_MATCH = 6102,
+
/**
* The signature provided in the "Sync-Signature" header is malformed or missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3109,6 +3663,7 @@ export enum TalerErrorCode {
*/
SYNC_BAD_SYNC_SIGNATURE = 6103,
+
/**
* The signature provided in the "Sync-Signature" header does not match the account, old or new Etags.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -3116,6 +3671,7 @@ export enum TalerErrorCode {
*/
SYNC_INVALID_SIGNATURE = 6104,
+
/**
* The "Content-length" field for the upload is not a number.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3123,6 +3679,7 @@ export enum TalerErrorCode {
*/
SYNC_MALFORMED_CONTENT_LENGTH = 6105,
+
/**
* The "Content-length" field for the upload is too big based on the server's terms of service.
* Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
@@ -3130,6 +3687,7 @@ export enum TalerErrorCode {
*/
SYNC_EXCESSIVE_CONTENT_LENGTH = 6106,
+
/**
* The server is out of memory to handle the upload. Trying again later may succeed.
* Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
@@ -3137,6 +3695,7 @@ export enum TalerErrorCode {
*/
SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH = 6107,
+
/**
* The uploaded data does not match the Etag.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3144,6 +3703,7 @@ export enum TalerErrorCode {
*/
SYNC_INVALID_UPLOAD = 6108,
+
/**
* HTTP server experienced a timeout while awaiting promised payment.
* Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
@@ -3151,6 +3711,7 @@ export enum TalerErrorCode {
*/
SYNC_PAYMENT_GENERIC_TIMEOUT = 6109,
+
/**
* Sync could not setup the payment request with its own backend.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3158,6 +3719,7 @@ export enum TalerErrorCode {
*/
SYNC_PAYMENT_CREATE_BACKEND_ERROR = 6110,
+
/**
* The sync service failed find the backup to be updated in its database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3165,6 +3727,7 @@ export enum TalerErrorCode {
*/
SYNC_PREVIOUS_BACKUP_UNKNOWN = 6111,
+
/**
* The "Content-length" field for the upload is missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3172,6 +3735,7 @@ export enum TalerErrorCode {
*/
SYNC_MISSING_CONTENT_LENGTH = 6112,
+
/**
* Sync had problems communicating with its payment backend.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -3179,6 +3743,7 @@ export enum TalerErrorCode {
*/
SYNC_GENERIC_BACKEND_ERROR = 6113,
+
/**
* Sync experienced a timeout communicating with its payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -3186,6 +3751,7 @@ export enum TalerErrorCode {
*/
SYNC_GENERIC_BACKEND_TIMEOUT = 6114,
+
/**
* The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
@@ -3193,6 +3759,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE = 7000,
+
/**
* The wallet encountered an unexpected exception. This is likely a bug in the wallet implementation.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3200,6 +3767,7 @@ export enum TalerErrorCode {
*/
WALLET_UNEXPECTED_EXCEPTION = 7001,
+
/**
* The wallet received a response from a server, but the response can't be parsed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3207,6 +3775,7 @@ export enum TalerErrorCode {
*/
WALLET_RECEIVED_MALFORMED_RESPONSE = 7002,
+
/**
* The wallet tried to make a network request, but it received no response.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3214,6 +3783,7 @@ export enum TalerErrorCode {
*/
WALLET_NETWORK_ERROR = 7003,
+
/**
* The wallet tried to make a network request, but it was throttled.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3221,6 +3791,7 @@ export enum TalerErrorCode {
*/
WALLET_HTTP_REQUEST_THROTTLED = 7004,
+
/**
* The wallet made a request to a service, but received an error response it does not know how to handle.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3228,6 +3799,7 @@ export enum TalerErrorCode {
*/
WALLET_UNEXPECTED_REQUEST_ERROR = 7005,
+
/**
* The denominations offered by the exchange are insufficient. Likely the exchange is badly configured or not maintained.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3235,6 +3807,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT = 7006,
+
/**
* The wallet does not support the operation requested by a client.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3242,6 +3815,7 @@ export enum TalerErrorCode {
*/
WALLET_CORE_API_OPERATION_UNKNOWN = 7007,
+
/**
* The given taler://pay URI is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3249,6 +3823,7 @@ export enum TalerErrorCode {
*/
WALLET_INVALID_TALER_PAY_URI = 7008,
+
/**
* The signature on a coin by the exchange's denomination key is invalid after unblinding it.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3256,6 +3831,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_COIN_SIGNATURE_INVALID = 7009,
+
/**
* The exchange does not know about the reserve (yet), and thus withdrawal can't progress.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3263,6 +3839,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_WITHDRAW_RESERVE_UNKNOWN_AT_EXCHANGE = 7010,
+
/**
* The wallet core service is not available.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3270,6 +3847,7 @@ export enum TalerErrorCode {
*/
WALLET_CORE_NOT_AVAILABLE = 7011,
+
/**
* The bank has aborted a withdrawal operation, and thus a withdrawal can't complete.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3277,6 +3855,7 @@ export enum TalerErrorCode {
*/
WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK = 7012,
+
/**
* An HTTP request made by the wallet timed out.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3284,6 +3863,7 @@ export enum TalerErrorCode {
*/
WALLET_HTTP_REQUEST_GENERIC_TIMEOUT = 7013,
+
/**
* The order has already been claimed by another wallet.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3291,6 +3871,7 @@ export enum TalerErrorCode {
*/
WALLET_ORDER_ALREADY_CLAIMED = 7014,
+
/**
* A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3298,6 +3879,7 @@ export enum TalerErrorCode {
*/
WALLET_WITHDRAWAL_GROUP_INCOMPLETE = 7015,
+
/**
* The signature on a coin by the exchange's denomination key (obtained through the merchant via a reward) is invalid after unblinding it.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3305,6 +3887,7 @@ export enum TalerErrorCode {
*/
WALLET_REWARD_COIN_SIGNATURE_INVALID = 7016,
+
/**
* The wallet does not implement a version of the bank integration API that is compatible with the version offered by the bank.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3312,6 +3895,7 @@ export enum TalerErrorCode {
*/
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE = 7017,
+
/**
* The wallet processed a taler://pay URI, but the merchant base URL in the downloaded contract terms does not match the merchant base URL derived from the URI.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3319,6 +3903,7 @@ export enum TalerErrorCode {
*/
WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH = 7018,
+
/**
* The merchant's signature on the contract terms is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3326,6 +3911,7 @@ export enum TalerErrorCode {
*/
WALLET_CONTRACT_TERMS_SIGNATURE_INVALID = 7019,
+
/**
* The contract terms given by the merchant are malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3333,6 +3919,7 @@ export enum TalerErrorCode {
*/
WALLET_CONTRACT_TERMS_MALFORMED = 7020,
+
/**
* A pending operation failed, and thus the request can't be completed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3340,6 +3927,7 @@ export enum TalerErrorCode {
*/
WALLET_PENDING_OPERATION_FAILED = 7021,
+
/**
* A payment was attempted, but the merchant had an internal server error (5xx).
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3347,6 +3935,7 @@ export enum TalerErrorCode {
*/
WALLET_PAY_MERCHANT_SERVER_ERROR = 7022,
+
/**
* The crypto worker failed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3354,6 +3943,7 @@ export enum TalerErrorCode {
*/
WALLET_CRYPTO_WORKER_ERROR = 7023,
+
/**
* The crypto worker received a bad request.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3361,6 +3951,7 @@ export enum TalerErrorCode {
*/
WALLET_CRYPTO_WORKER_BAD_REQUEST = 7024,
+
/**
* A KYC step is required before withdrawal can proceed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3368,6 +3959,7 @@ export enum TalerErrorCode {
*/
WALLET_WITHDRAWAL_KYC_REQUIRED = 7025,
+
/**
* The wallet does not have sufficient balance to create a deposit group.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3375,6 +3967,7 @@ export enum TalerErrorCode {
*/
WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE = 7026,
+
/**
* The wallet does not have sufficient balance to create a peer push payment.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3382,6 +3975,7 @@ export enum TalerErrorCode {
*/
WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE = 7027,
+
/**
* The wallet does not have sufficient balance to pay for an invoice.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3389,6 +3983,7 @@ export enum TalerErrorCode {
*/
WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE = 7028,
+
/**
* A group of refresh operations has errors and will be tried again later.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3396,6 +3991,7 @@ export enum TalerErrorCode {
*/
WALLET_REFRESH_GROUP_INCOMPLETE = 7029,
+
/**
* The exchange's self-reported base URL does not match the one that the wallet is using.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3403,6 +3999,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_BASE_URL_MISMATCH = 7030,
+
/**
* The order has already been paid by another wallet.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3410,6 +4007,7 @@ export enum TalerErrorCode {
*/
WALLET_ORDER_ALREADY_PAID = 7031,
+
/**
* An exchange that is required for some request is currently not available.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3417,6 +4015,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_UNAVAILABLE = 7032,
+
/**
* An exchange entry is still used by the exchange, thus it can't be deleted without purging.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3424,6 +4023,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_ENTRY_USED = 7033,
+
/**
* The wallet database is unavailable and the wallet thus is not operational.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3431,6 +4031,23 @@ export enum TalerErrorCode {
*/
WALLET_DB_UNAVAILABLE = 7034,
+
+ /**
+ * A taler:// URI is malformed and can't be parsed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_TALER_URI_MALFORMED = 7035,
+
+
+ /**
+ * A wallet-core request was cancelled and thus can't provide a response.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CORE_REQUEST_CANCELLED = 7036,
+
+
/**
* We encountered a timeout with our payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -3438,6 +4055,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_BACKEND_TIMEOUT = 8000,
+
/**
* The backend requested payment, but the request is malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3445,6 +4063,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST = 8001,
+
/**
* The backend got an unexpected reply from the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -3452,6 +4071,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_BACKEND_ERROR = 8002,
+
/**
* The "Content-length" field for the upload is missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3459,6 +4079,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH = 8003,
+
/**
* The "Content-length" field for the upload is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3466,6 +4087,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH = 8004,
+
/**
* The backend failed to setup an order with the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -3473,6 +4095,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_ORDER_CREATE_BACKEND_ERROR = 8005,
+
/**
* The backend was not authorized to check for payment with the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3480,6 +4103,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED = 8006,
+
/**
* The backend could not check payment status with the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3487,6 +4111,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED = 8007,
+
/**
* The Anastasis provider could not be reached.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3494,6 +4119,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_PROVIDER_UNREACHABLE = 8008,
+
/**
* HTTP server experienced a timeout while awaiting promised payment.
* Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
@@ -3501,6 +4127,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_PAYMENT_GENERIC_TIMEOUT = 8009,
+
/**
* The key share is unknown to the provider.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3508,6 +4135,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UNKNOWN = 8108,
+
/**
* The authorization method used for the key share is no longer supported by the provider.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3515,6 +4143,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED = 8109,
+
/**
* The client needs to respond to the challenge.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -3522,6 +4151,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED = 8110,
+
/**
* The client's response to the challenge was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -3529,6 +4159,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_FAILED = 8111,
+
/**
* The backend is not aware of having issued the provided challenge code. Either this is the wrong code, or it has expired.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3536,6 +4167,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_UNKNOWN = 8112,
+
/**
* The backend failed to initiate the authorization process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3543,6 +4175,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED = 8114,
+
/**
* The authorization succeeded, but the key share is no longer available.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3550,6 +4183,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_KEY_SHARE_GONE = 8115,
+
/**
* The backend forgot the order we asked the client to pay for
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -3557,6 +4191,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_ORDER_DISAPPEARED = 8116,
+
/**
* The backend itself reported a bad exchange interaction.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -3564,6 +4199,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD = 8117,
+
/**
* The backend reported a payment status we did not expect.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3571,6 +4207,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS = 8118,
+
/**
* The backend failed to setup the order for payment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -3578,6 +4215,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR = 8119,
+
/**
* The decryption of the key share failed with the provided key.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3585,6 +4223,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_DECRYPTION_FAILED = 8120,
+
/**
* The request rate is too high. The server is refusing requests to guard against brute-force attacks.
* Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
@@ -3592,6 +4231,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_RATE_LIMITED = 8121,
+
/**
* A request to issue a challenge is not valid for this authentication method.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3599,6 +4239,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_WRONG_METHOD = 8123,
+
/**
* The backend failed to store the key share because the UUID is already in use.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3606,6 +4247,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS = 8150,
+
/**
* The backend failed to store the key share because the authorization method is not supported.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3613,6 +4255,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UPLOAD_METHOD_NOT_SUPPORTED = 8151,
+
/**
* The provided phone number is not an acceptable number.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3620,6 +4263,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_SMS_PHONE_INVALID = 8200,
+
/**
* Failed to run the SMS transmission helper process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3627,6 +4271,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_SMS_HELPER_EXEC_FAILED = 8201,
+
/**
* Provider failed to send SMS. Helper terminated with a non-successful result.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3634,6 +4279,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_SMS_HELPER_COMMAND_FAILED = 8202,
+
/**
* The provided email address is not an acceptable address.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3641,6 +4287,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_EMAIL_INVALID = 8210,
+
/**
* Failed to run the E-mail transmission helper process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3648,6 +4295,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_EMAIL_HELPER_EXEC_FAILED = 8211,
+
/**
* Provider failed to send E-mail. Helper terminated with a non-successful result.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3655,6 +4303,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_EMAIL_HELPER_COMMAND_FAILED = 8212,
+
/**
* The provided postal address is not an acceptable address.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3662,6 +4311,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POST_INVALID = 8220,
+
/**
* Failed to run the mail transmission helper process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3669,6 +4319,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POST_HELPER_EXEC_FAILED = 8221,
+
/**
* Provider failed to send mail. Helper terminated with a non-successful result.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3676,6 +4327,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POST_HELPER_COMMAND_FAILED = 8222,
+
/**
* The provided IBAN address is not an acceptable IBAN.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3683,6 +4335,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_IBAN_INVALID = 8230,
+
/**
* The provider has not yet received the IBAN wire transfer authorizing the disclosure of the key share.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -3690,6 +4343,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_IBAN_MISSING_TRANSFER = 8231,
+
/**
* The backend did not find a TOTP key in the data provided.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3697,6 +4351,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TOTP_KEY_MISSING = 8240,
+
/**
* The key provided does not satisfy the format restrictions for an Anastasis TOTP key.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3704,6 +4359,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TOTP_KEY_INVALID = 8241,
+
/**
* The given if-none-match header is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3711,6 +4367,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_BAD_IF_NONE_MATCH = 8301,
+
/**
* The server is out of memory to handle the upload. Trying again later may succeed.
* Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
@@ -3718,6 +4375,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_OUT_OF_MEMORY_ON_CONTENT_LENGTH = 8304,
+
/**
* The signature provided in the "Anastasis-Policy-Signature" header is malformed or missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3725,6 +4383,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_BAD_SIGNATURE = 8305,
+
/**
* The given if-match header is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3732,6 +4391,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_BAD_IF_MATCH = 8306,
+
/**
* The uploaded data does not match the Etag.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3739,6 +4399,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_INVALID_UPLOAD = 8307,
+
/**
* The provider is unaware of the requested policy.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3746,6 +4407,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_NOT_FOUND = 8350,
+
/**
* The given action is invalid for the current state of the reducer.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3753,6 +4415,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_ACTION_INVALID = 8400,
+
/**
* The given state of the reducer is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3760,6 +4423,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_STATE_INVALID = 8401,
+
/**
* The given input to the reducer is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3767,6 +4431,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_INVALID = 8402,
+
/**
* The selected authentication method does not work for the Anastasis provider.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3774,6 +4439,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_AUTHENTICATION_METHOD_NOT_SUPPORTED = 8403,
+
/**
* The given input and action do not work for the current state.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3781,6 +4447,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE = 8404,
+
/**
* We experienced an unexpected failure interacting with the backend.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3788,6 +4455,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_BACKEND_FAILURE = 8405,
+
/**
* The contents of a resource file did not match our expectations.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3795,6 +4463,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_RESOURCE_MALFORMED = 8406,
+
/**
* A required resource file is missing.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3802,6 +4471,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_RESOURCE_MISSING = 8407,
+
/**
* An input did not match the regular expression.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3809,6 +4479,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_REGEX_FAILED = 8408,
+
/**
* An input did not match the custom validation logic.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3816,6 +4487,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED = 8409,
+
/**
* Our attempts to download the recovery document failed with all providers. Most likely the personal information you entered differs from the information you provided during the backup process and you should go back to the previous step. Alternatively, if you used a backup provider that is unknown to this application, you should add that provider manually.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3823,6 +4495,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED = 8410,
+
/**
* Anastasis provider reported a fatal failure.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3830,6 +4503,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED = 8411,
+
/**
* Anastasis provider failed to respond to the configuration request.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3837,6 +4511,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED = 8412,
+
/**
* The policy we downloaded is malformed. Must have been a client error while creating the backup.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3844,6 +4519,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_POLICY_MALFORMED = 8413,
+
/**
* We failed to obtain the policy, likely due to a network issue.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3851,6 +4527,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_NETWORK_FAILED = 8414,
+
/**
* The recovered secret did not match the required syntax.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3858,6 +4535,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_SECRET_MALFORMED = 8415,
+
/**
* The challenge data provided is too large for the available providers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3865,6 +4543,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_CHALLENGE_DATA_TOO_BIG = 8416,
+
/**
* The provided core secret is too large for some of the providers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3872,6 +4551,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_SECRET_TOO_BIG = 8417,
+
/**
* The provider returned in invalid configuration.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3879,6 +4559,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG = 8418,
+
/**
* The reducer encountered an internal error, likely a bug that needs to be reported.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3886,6 +4567,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INTERNAL_ERROR = 8419,
+
/**
* The reducer already synchronized with all providers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3893,13 +4575,39 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_PROVIDERS_ALREADY_SYNCED = 8420,
+
/**
- * The donau failed to perform the operation as it could not find the private keys. This is a problem with the donau setup, not with the client's request.
+ * The Donau failed to perform the operation as it could not find the private keys. This is a problem with the Donau setup, not with the client's request.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
* (A value of 0 indicates that the error is generated client-side).
*/
DONAU_GENERIC_KEYS_MISSING = 8607,
+
+ /**
+ * The signature of the charity key is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_CHARITY_SIGNATURE_INVALID = 8608,
+
+
+ /**
+ * The charity is unknown.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_CHARITY_NOT_FOUND = 8609,
+
+
+ /**
+ * The donation amount specified in the request exceeds the limit of the charity.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_EXCEEDING_DONATION_LIMIT = 8610,
+
+
/**
* A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3907,6 +4615,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_NEXUS_GENERIC_ERROR = 9000,
+
/**
* An uncaught exception happened in the LibEuFin nexus service.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3914,6 +4623,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION = 9001,
+
/**
* A generic error happened in the LibEuFin sandbox. See the enclose details JSON for more information.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3921,6 +4631,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_SANDBOX_GENERIC_ERROR = 9500,
+
/**
* An uncaught exception happened in the LibEuFin sandbox service.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3928,6 +4639,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_SANDBOX_UNCAUGHT_EXCEPTION = 9501,
+
/**
* This validation method is not supported by the service.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3935,6 +4647,7 @@ export enum TalerErrorCode {
*/
TALDIR_METHOD_NOT_SUPPORTED = 9600,
+
/**
* Number of allowed attempts for initiating a challenge exceeded.
* Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
@@ -3942,6 +4655,7 @@ export enum TalerErrorCode {
*/
TALDIR_REGISTER_RATE_LIMITED = 9601,
+
/**
* The client is unknown or unauthorized.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3949,6 +4663,7 @@ export enum TalerErrorCode {
*/
CHALLENGER_GENERIC_CLIENT_UNKNOWN = 9750,
+
/**
* The client is not authorized to use the given redirect URI.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -3956,6 +4671,7 @@ export enum TalerErrorCode {
*/
CHALLENGER_GENERIC_CLIENT_FORBIDDEN_BAD_REDIRECT_URI = 9751,
+
/**
* The service failed to execute its helper process to send the challenge.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3963,6 +4679,7 @@ export enum TalerErrorCode {
*/
CHALLENGER_HELPER_EXEC_FAILED = 9752,
+
/**
* The grant is unknown to the service (it could also have expired).
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3970,6 +4687,7 @@ export enum TalerErrorCode {
*/
CHALLENGER_GRANT_UNKNOWN = 9753,
+
/**
* The code given is not even well-formed.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -3977,6 +4695,7 @@ export enum TalerErrorCode {
*/
CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE = 9754,
+
/**
* The service is not aware of the referenced validation process.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3984,6 +4703,7 @@ export enum TalerErrorCode {
*/
CHALLENGER_GENERIC_VALIDATION_UNKNOWN = 9755,
+
/**
* The code given is not valid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -3991,6 +4711,7 @@ export enum TalerErrorCode {
*/
CHALLENGER_CLIENT_FORBIDDEN_INVALID_CODE = 9756,
+
/**
* Too many attempts have been made, validation is temporarily disabled for this address.
* Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
@@ -3998,6 +4719,7 @@ export enum TalerErrorCode {
*/
CHALLENGER_TOO_MANY_ATTEMPTS = 9757,
+
/**
* The PIN code provided is incorrect.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -4005,6 +4727,7 @@ export enum TalerErrorCode {
*/
CHALLENGER_INVALID_PIN = 9758,
+
/**
* The token cannot be valid as no address was ever provided by the client.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -4012,10 +4735,13 @@ export enum TalerErrorCode {
*/
CHALLENGER_MISSING_ADDRESS = 9759,
+
/**
* End of error code range.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
END = 9999,
+
+
}
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 7cc703fd6..392e7149c 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -1335,6 +1335,7 @@ export type AmountString = string & { [__amount_str]: true };
export type Base32String = string;
export type EddsaSignatureString = string;
export type EddsaPublicKeyString = string;
+export type EddsaPrivateKeyString = string;
export type CoinPublicKeyString = string;
export const codecForDenomination = (): Codec<ExchangeDenomination> =>
@@ -2359,3 +2360,58 @@ export const codecForBankConversionInfoConfig =
codecForCurrencySpecificiation(),
)
.build("BankConversionInfoConfig");
+
+export interface DenominationExpiredMessage {
+ // Taler error code. Note that beyond
+ // expiration this message format is also
+ // used if the key is not yet valid, or
+ // has been revoked.
+ code: number;
+
+ // Signature by the exchange over a
+ // TALER_DenominationExpiredAffirmationPS.
+ // Must have purpose TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED.
+ exchange_sig: EddsaSignatureString;
+
+ // Public key of the exchange used to create
+ // the 'exchange_sig.
+ exchange_pub: EddsaPublicKeyString;
+
+ // Hash of the denomination public key that is unknown.
+ h_denom_pub: HashCodeString;
+
+ // When was the signature created.
+ timestamp: TalerProtocolTimestamp;
+
+ // What kind of operation was requested that now
+ // failed?
+ oper: string;
+}
+
+export const codecForDenominationExpiredMessage = () =>
+ buildCodecForObject<DenominationExpiredMessage>()
+ .property("code", codecForNumber())
+ .property("exchange_sig", codecForString())
+ .property("exchange_pub", codecForString())
+ .property("h_denom_pub", codecForString())
+ .property("timestamp", codecForTimestamp)
+ .property("oper", codecForString())
+ .build("DenominationExpiredMessage");
+
+export interface CoinHistoryResponse {
+ // Current balance of the coin.
+ balance: AmountString;
+
+ // Hash of the coin's denomination.
+ h_denom_pub: HashCodeString;
+
+ // Transaction history for the coin.
+ history: any[];
+}
+
+export const codecForCoinHistoryResponse = () =>
+ buildCodecForObject<CoinHistoryResponse>()
+ .property("balance", codecForAmountString())
+ .property("h_denom_pub", codecForString())
+ .property("history", codecForAny())
+ .build("CoinHistoryResponse");
diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts
index 83c0044be..2bd7b355f 100644
--- a/packages/taler-util/src/talerconfig.ts
+++ b/packages/taler-util/src/talerconfig.ts
@@ -23,13 +23,12 @@
/**
* Imports
*/
-import { AmountJson } from "./amounts.js";
-import { Amounts } from "./amounts.js";
+import { AmountJson, Amounts } from "./amounts.js";
import { Logger } from "./logging.js";
-import nodejs_path from "path";
-import nodejs_os from "os";
import nodejs_fs from "fs";
+import nodejs_os from "os";
+import nodejs_path from "path";
const logger = new Logger("talerconfig.ts");
@@ -76,6 +75,54 @@ interface Section {
type SectionMap = { [sectionName: string]: Section };
+/**
+ * Different projects use the GNUnet/Taler-Style config.
+ *
+ * The config source determines where to locate the configuration.
+ */
+export interface ConfigSource {
+ projectName: string;
+ componentName: string;
+ installPathBinary: string;
+ baseConfigVarname: string;
+ prefixVarname: string;
+}
+
+export type ConfigSourceDef = { [x: string]: ConfigSource | undefined };
+
+export const ConfigSources = {
+ ["taler"]: {
+ projectName: "taler",
+ componentName: "taler",
+ installPathBinary: "taler-config",
+ baseConfigVarname: "TALER_BASE_CONFIG",
+ prefixVarname: "TALER_PREFIX",
+ } satisfies ConfigSource,
+ ["libeufin-bank"]: {
+ projectName: "libeufin",
+ componentName: "libeufin-bank",
+ installPathBinary: "libeufin-bank",
+ baseConfigVarname: "LIBEUFIN_BASE_CONFIG",
+ prefixVarname: "LIBEUFIN_PREFIX",
+ } satisfies ConfigSource,
+ ["libeufin-nexus"]: {
+ projectName: "libeufin",
+ componentName: "libeufin-nexus",
+ installPathBinary: "libeufin-nexus",
+ baseConfigVarname: "LIBEUFIN_BASE_CONFIG",
+ prefixVarname: "LIBEUFIN_PREFIX",
+ } satisfies ConfigSource,
+ ["gnunet"]: {
+ projectName: "gnunet",
+ componentName: "gnunet",
+ installPathBinary: "gnunet-config",
+ baseConfigVarname: "GNUNET_BASE_CONFIG",
+ prefixVarname: "GNUNET_PREFIX",
+ } satisfies ConfigSource,
+} satisfies ConfigSourceDef;
+
+const defaultConfigSource: ConfigSource = ConfigSources.taler;
+
export class ConfigValue<T> {
constructor(
private sectionName: string,
@@ -215,7 +262,7 @@ export function pathsub(
return s;
}
-export interface LoadOptions {
+interface LoadOptions {
filename?: string;
banDirectives?: boolean;
}
@@ -310,6 +357,14 @@ export class Configuration {
private nestLevel = 0;
+ /**
+ * Does the entrypoint config file contain complex
+ * directives?
+ */
+ private entrypointIsComplex: boolean = false;
+
+ constructor(private configSource: ConfigSource = defaultConfigSource) {}
+
private loadFromFilename(
filename: string,
isDefaultSource: boolean,
@@ -434,6 +489,9 @@ export class Configuration {
`invalid configuration, directive in ${fn}:${lineNo} forbidden`,
);
}
+ if (!isDefaultSource) {
+ this.entrypointIsComplex = true;
+ }
const directive = directiveMatch[1].toLowerCase();
switch (directive) {
case "inline": {
@@ -521,10 +579,6 @@ export class Configuration {
}
}
- loadFromString(s: string, opts: LoadOptions = {}): void {
- return this.internalLoadFromString(s, false, opts);
- }
-
private provideSection(section: string): Section {
const secNorm = section.toUpperCase();
if (this.sectionMap[secNorm]) {
@@ -653,7 +707,7 @@ export class Configuration {
);
}
- loadDefaultsFromDir(dirname: string): void {
+ private loadDefaultsFromDir(dirname: string): void {
const files = nodejs_fs.readdirSync(dirname);
for (const f of files) {
const fn = nodejs_path.join(dirname, f);
@@ -662,26 +716,28 @@ export class Configuration {
}
private loadDefaults(): void {
- let baseConfigDir = process.env["TALER_BASE_CONFIG"];
+ const { projectName, prefixVarname, baseConfigVarname, installPathBinary } =
+ this.configSource;
+ let baseConfigDir = process.env[baseConfigVarname];
if (!baseConfigDir) {
/* Try to locate the configuration based on the location
* of the taler-config binary. */
- const path = which("taler-config");
+ const path = which(installPathBinary);
if (path) {
baseConfigDir = nodejs_fs.realpathSync(
- nodejs_path.dirname(path) + "/../share/taler/config.d",
+ nodejs_path.dirname(path) + `/../share/${projectName}/config.d`,
);
}
}
if (!baseConfigDir) {
- baseConfigDir = "/usr/share/taler/config.d";
+ baseConfigDir = `/usr/share/${projectName}/config.d`;
}
- let installPrefix = process.env["TALER_PREFIX"];
+ let installPrefix = process.env[prefixVarname];
if (!installPrefix) {
/* Try to locate install path based on the location
* of the taler-config binary. */
- const path = which("taler-config");
+ const path = which(installPathBinary);
if (path) {
installPrefix = nodejs_fs.realpathSync(
nodejs_path.dirname(path) + "/..",
@@ -695,12 +751,12 @@ export class Configuration {
this.setStringSystemDefault(
"PATHS",
"LIBEXECDIR",
- `${installPrefix}/taler/libexec/`,
+ `${installPrefix}/${projectName}/libexec/`,
);
this.setStringSystemDefault(
"PATHS",
"DOCDIR",
- `${installPrefix}/share/doc/taler/`,
+ `${installPrefix}/share/doc/${projectName}/`,
);
this.setStringSystemDefault(
"PATHS",
@@ -717,58 +773,80 @@ export class Configuration {
this.setStringSystemDefault(
"PATHS",
"LIBDIR",
- `${installPrefix}/lib/taler/`,
+ `${installPrefix}/lib/${projectName}/`,
);
this.setStringSystemDefault(
"PATHS",
"DATADIR",
- `${installPrefix}/share/taler/`,
+ `${installPrefix}/share/${projectName}/`,
);
this.loadDefaultsFromDir(baseConfigDir);
}
- getDefaultConfigFilename(): string | undefined {
+ private findDefaultConfigFilename(): string | undefined {
const xdg = process.env["XDG_CONFIG_HOME"];
const home = process.env["HOME"];
let fn: string | undefined;
+ const { projectName, componentName } = this.configSource;
if (xdg) {
- fn = nodejs_path.join(xdg, "taler.conf");
+ fn = nodejs_path.join(xdg, `${componentName}.conf`);
} else if (home) {
- fn = nodejs_path.join(home, ".config/taler.conf");
+ fn = nodejs_path.join(home, `.config/${componentName}.conf`);
}
if (fn && nodejs_fs.existsSync(fn)) {
return fn;
}
- const etc1 = "/etc/taler.conf";
+ const etc1 = `/etc/${componentName}.conf`;
if (nodejs_fs.existsSync(etc1)) {
return etc1;
}
- const etc2 = "/etc/taler/taler.conf";
+ const etc2 = `/etc/${projectName}/${componentName}.conf`;
if (nodejs_fs.existsSync(etc2)) {
return etc2;
}
return undefined;
}
- static load(filename?: string): Configuration {
- const cfg = new Configuration();
+ static load(
+ filename?: string,
+ configSource?: ConfigSource | string,
+ ): Configuration {
+ let cs: ConfigSource;
+ if (configSource == null) {
+ cs = defaultConfigSource;
+ } else if (typeof configSource === "string") {
+ if (configSource in ConfigSources) {
+ cs = ConfigSources[configSource as keyof typeof ConfigSources];
+ } else {
+ throw Error("invalid config source");
+ }
+ } else {
+ cs = configSource;
+ }
+ const cfg = new Configuration(cs);
cfg.loadDefaults();
if (filename) {
cfg.loadFromFilename(filename, false);
+ cfg.hintEntrypoint = filename;
} else {
- const fn = cfg.getDefaultConfigFilename();
+ const fn = cfg.findDefaultConfigFilename();
if (fn) {
// It's the default filename for the main config file,
// but we don't consider the values default values.
cfg.loadFromFilename(fn, false);
+ cfg.hintEntrypoint = fn;
}
}
- cfg.hintEntrypoint = filename;
return cfg;
}
stringify(opts: StringifyOptions = {}): string {
+ if (opts.excludeDefaults && this.entrypointIsComplex) {
+ throw Error(
+ "unable to do diff serialization of config file, as entry point contains complex directives",
+ );
+ }
let s = "";
if (opts.diagnostics) {
s += "# Configuration file diagnostics\n";
@@ -824,7 +902,20 @@ export class Configuration {
return s;
}
- write(filename: string, opts: { excludeDefaults?: boolean } = {}): void {
+ write(opts: { excludeDefaults?: boolean } = {}): void {
+ const filename = this.hintEntrypoint;
+ if (!filename) {
+ throw Error(
+ "unknown configuration entrypoing, unable to write back config file",
+ );
+ }
+ nodejs_fs.writeFileSync(
+ filename,
+ this.stringify({ excludeDefaults: opts.excludeDefaults }),
+ );
+ }
+
+ writeTo(filename: string, opts: { excludeDefaults?: boolean } = {}): void {
nodejs_fs.writeFileSync(
filename,
this.stringify({ excludeDefaults: opts.excludeDefaults }),
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
index dbd175fe5..7f10d21fd 100644
--- a/packages/taler-util/src/taleruri.test.ts
+++ b/packages/taler-util/src/taleruri.test.ts
@@ -17,6 +17,7 @@
import test from "ava";
import { AmountString } from "./taler-types.js";
import {
+ parseAddExchangeUri,
parseDevExperimentUri,
parsePayPullUri,
parsePayPushUri,
@@ -26,6 +27,7 @@ import {
parseRestoreUri,
parseWithdrawExchangeUri,
parseWithdrawUri,
+ stringifyAddExchange,
stringifyDevExperimentUri,
stringifyPayPullUri,
stringifyPayPushUri,
@@ -506,6 +508,51 @@ test("taler withdraw exchange URI with amount (stringify)", (t) => {
);
});
+
+/**
+ * 5.13 action: add-exchange https://lsd.gnunet.org/lsd0006/#name-action-add-exchange
+ */
+
+test("taler add exchange URI (parse)", (t) => {
+ {
+ const r1 = parseAddExchangeUri(
+ "taler://add-exchange/exchange.example.com/",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.exchangeBaseUrl,
+ "https://exchange.example.com/",
+ );
+ }
+ {
+ const r2 = parseAddExchangeUri(
+ "taler://add-exchange/exchanges.example.com/api/",
+ );
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r2.exchangeBaseUrl,
+ "https://exchanges.example.com/api/",
+ );
+ }
+
+});
+
+test("taler add exchange URI (stringify)", (t) => {
+ const url = stringifyAddExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ });
+ t.deepEqual(
+ url,
+ "taler://add-exchange/exchange.demo.taler.net/",
+ );
+});
+
/**
* wrong uris
*/
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
index 97b82c061..b4f9db6ef 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -25,9 +25,10 @@
*/
import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { canonicalizeBaseUrl } from "./helpers.js";
+import { opFixedSuccess, opKnownTalerFailure } from "./operation.js";
+import { TalerErrorCode } from "./taler-error-codes.js";
import { AmountString } from "./taler-types.js";
import { URL, URLSearchParams } from "./url.js";
-
/**
* A parsed taler URI.
*/
@@ -40,7 +41,8 @@ export type TalerUri =
| BackupRestoreUri
| RefundUriResult
| WithdrawUriResult
- | WithdrawExchangeUri;
+ | WithdrawExchangeUri
+ | AddExchangeUri;
declare const __action_str: unique symbol;
export type TalerUriString = string & { [__action_str]: true };
@@ -126,19 +128,26 @@ export interface WithdrawExchangeUri {
amount?: AmountString;
}
+export interface AddExchangeUri {
+ type: TalerUriAction.AddExchange;
+ exchangeBaseUrl: string;
+}
+
/**
* Parse a taler[+http]://withdraw URI.
* Return undefined if not passed a valid URI.
*/
-export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
- const pi = parseProtoInfo(s, "withdraw");
- if (!pi) {
- return undefined;
+export function parseWithdrawUriWithError(s: string) {
+ const pi = parseProtoInfoWithError(s, "withdraw");
+ if (pi.type === "fail") {
+ return pi;
}
- const parts = pi.rest.split("/");
+ const parts = pi.body.rest.split("/");
if (parts.length < 2) {
- return undefined;
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
}
const host = parts[0].toLowerCase();
@@ -153,11 +162,71 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
const withdrawId = parts[parts.length - 1];
const p = [host, ...pathSegments].join("/");
- return {
+ const result: WithdrawUriResult = {
type: TalerUriAction.Withdraw,
- bankIntegrationApiBaseUrl: canonicalizeBaseUrl(`${pi.innerProto}://${p}/`),
+ bankIntegrationApiBaseUrl: canonicalizeBaseUrl(
+ `${pi.body.innerProto}://${p}/`,
+ ),
withdrawalOperationId: withdrawId,
};
+ return opFixedSuccess(result);
+}
+
+/**
+ *
+ * @deprecated use parseWithdrawUriWithError
+ */
+export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
+ const r = parseWithdrawUriWithError(s);
+ if (r.type === "fail") return undefined;
+ return r.body;
+}
+
+/**
+ * Parse a taler[+http]://withdraw URI.
+ * Return undefined if not passed a valid URI.
+ */
+export function parseAddExchangeUriWithError(s: string) {
+ const pi = parseProtoInfoWithError(s, "add-exchange");
+ if (pi.type === "fail") {
+ return pi;
+ }
+ const parts = pi.body.rest.split("/");
+
+ if (parts.length < 2) {
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
+ }
+
+ const host = parts[0].toLowerCase();
+ const pathSegments = parts.slice(1, parts.length - 1);
+ /**
+ * The statement below does not tolerate a slash-ended URI.
+ * This results in (1) the withdrawalId being passed as the
+ * empty string, and (2) the bankIntegrationApi ending with the
+ * actual withdrawal operation ID. That can be fixed by
+ * trimming the parts-list. FIXME
+ */
+ const p = [host, ...pathSegments].join("/");
+
+ const result: AddExchangeUri = {
+ type: TalerUriAction.AddExchange,
+ exchangeBaseUrl: canonicalizeBaseUrl(
+ `${pi.body.innerProto}://${p}/`,
+ ),
+ };
+ return opFixedSuccess(result);
+}
+
+/**
+ *
+ * @deprecated use parseWithdrawUriWithError
+ */
+export function parseAddExchangeUri(s: string): AddExchangeUri | undefined {
+ const r = parseAddExchangeUriWithError(s);
+ if (r.type === "fail") return undefined;
+ return r.body;
}
/**
@@ -187,6 +256,7 @@ export enum TalerUriAction {
Restore = "restore",
DevExperiment = "dev-experiment",
WithdrawExchange = "withdraw-exchange",
+ AddExchange = "add-exchange",
}
interface TalerUriProtoInfo {
@@ -215,6 +285,34 @@ function parseProtoInfo(
}
}
+function parseProtoInfoWithError(s: string, action: string) {
+ if (
+ !s.toLowerCase().startsWith("taler://") &&
+ !s.toLowerCase().startsWith("taler+http://")
+ ) {
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
+ }
+ const pfxPlain = `taler://${action}/`;
+ const pfxHttp = `taler+http://${action}/`;
+ if (s.toLowerCase().startsWith(pfxPlain)) {
+ return opFixedSuccess({
+ innerProto: "https",
+ rest: s.substring(pfxPlain.length),
+ });
+ } else if (s.toLowerCase().startsWith(pfxHttp)) {
+ return opFixedSuccess({
+ innerProto: "http",
+ rest: s.substring(pfxHttp.length),
+ });
+ } else {
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
+ }
+}
+
type Parser = (s: string) => TalerUri | undefined;
const parsers: { [A in TalerUriAction]: Parser } = {
[TalerUriAction.Pay]: parsePayUri,
@@ -226,6 +324,7 @@ const parsers: { [A in TalerUriAction]: Parser } = {
[TalerUriAction.Withdraw]: parseWithdrawUri,
[TalerUriAction.DevExperiment]: parseDevExperimentUri,
[TalerUriAction.WithdrawExchange]: parseWithdrawExchangeUri,
+ [TalerUriAction.AddExchange]: parseAddExchangeUri,
};
export function parseTalerUri(string: string): TalerUri | undefined {
@@ -269,6 +368,9 @@ export function stringifyTalerUri(uri: TalerUri): string {
case TalerUriAction.WithdrawExchange: {
return stringifyWithdrawExchange(uri);
}
+ case TalerUriAction.AddExchange: {
+ return stringifyAddExchange(uri);
+ }
}
}
@@ -548,6 +650,13 @@ export function stringifyWithdrawExchange({
return `${proto}://withdraw-exchange/${path}${exchangePub ?? ""}${query}`;
}
+export function stringifyAddExchange({
+ exchangeBaseUrl,
+}: Omit<AddExchangeUri, "type">): string {
+ const { proto, path } = getUrlInfo(exchangeBaseUrl);
+ return `${proto}://add-exchange/${path}`;
+}
+
export function stringifyDevExperimentUri({
devExperimentId,
}: Omit<DevExperimentUri, "type">): string {
diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts
index 2e24856ee..95b4911a0 100644
--- a/packages/taler-util/src/time.ts
+++ b/packages/taler-util/src/time.ts
@@ -604,6 +604,9 @@ export function durationAdd(d1: Duration, d2: Duration): Duration {
export const codecForAbsoluteTime: Codec<AbsoluteTime> = {
decode(x: any, c?: Context): AbsoluteTime {
+ if (x === undefined) {
+ throw Error(`got undefined and expected absolute time at ${renderContext(c)}`);
+ }
const t_ms = x.t_ms;
if (typeof t_ms === "string") {
if (t_ms === "never") {
@@ -619,6 +622,9 @@ export const codecForAbsoluteTime: Codec<AbsoluteTime> = {
export const codecForTimestamp: Codec<TalerProtocolTimestamp> = {
decode(x: any, c?: Context): TalerProtocolTimestamp {
// Compatibility, should be removed soon.
+ if (x === undefined) {
+ throw Error(`got undefined and expected timestamp at ${renderContext(c)}`);
+ }
const t_ms = x.t_ms;
if (typeof t_ms === "string") {
if (t_ms === "never") {
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index 8c4c2c7ed..cee3de9fa 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -151,6 +151,7 @@ export enum TransactionMinorState {
RefundAvailable = "refund-available",
AcceptRefund = "accept-refund",
PaidByOther = "paid-by-other",
+ CompletedByOtherWallet = "completed-by-other-wallet",
}
export enum TransactionAction {
@@ -214,7 +215,6 @@ export type Transaction =
| TransactionWithdrawal
| TransactionPayment
| TransactionRefund
- | TransactionReward
| TransactionRefresh
| TransactionDeposit
| TransactionPeerPullCredit
@@ -222,7 +222,8 @@ export type Transaction =
| TransactionPeerPushCredit
| TransactionPeerPushDebit
| TransactionInternalWithdrawal
- | TransactionRecoup;
+ | TransactionRecoup
+ | TransactionDenomLoss;
export enum TransactionType {
Withdrawal = "withdrawal",
@@ -230,13 +231,13 @@ export enum TransactionType {
Payment = "payment",
Refund = "refund",
Refresh = "refresh",
- Reward = "reward",
Deposit = "deposit",
PeerPushDebit = "peer-push-debit",
PeerPushCredit = "peer-push-credit",
PeerPullDebit = "peer-pull-debit",
PeerPullCredit = "peer-pull-credit",
Recoup = "recoup",
+ DenomLoss = "denom-loss",
}
export enum WithdrawalType {
@@ -298,6 +299,22 @@ interface WithdrawalDetailsForTalerBankIntegrationApi {
exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
}
+export enum DenomLossEventType {
+ DenomExpired = "denom-expired",
+ DenomVanished = "denom-vanished",
+ DenomUnoffered = "denom-unoffered",
+}
+
+/**
+ * A transaction to indicate financial loss due to denominations
+ * that became unusable for deposits.
+ */
+export interface TransactionDenomLoss extends TransactionCommon {
+ type: TransactionType.DenomLoss;
+ lossEventType: DenomLossEventType;
+ exchangeBaseUrl: string;
+}
+
/**
* A withdrawal transaction (either bank-integrated or manual).
*/
@@ -623,23 +640,6 @@ export interface TransactionRefund extends TransactionCommon {
paymentInfo: RefundPaymentInfo | undefined;
}
-export interface TransactionReward extends TransactionCommon {
- type: TransactionType.Reward;
-
- // Raw amount of the tip, without extra fees that apply
- amountRaw: AmountString;
-
- /**
- * More information about the merchant
- */
- // merchant: MerchantInfo;
-
- // Amount will be (or was) added to the wallet's balance after fees and refreshing
- amountEffective: AmountString;
-
- merchantBaseUrl: string;
-}
-
/**
* A transaction shown for refreshes.
* Only shown for (1) refreshes not associated with other transactions
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 7b6da8a40..b9fd24754 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -50,6 +50,7 @@ import {
CurrencySpecification,
TemplateParams,
WithdrawalOperationStatus,
+ canonicalizeBaseUrl,
} from "./index.js";
import { VersionMatchResult } from "./libtool-version.js";
import { PaytoUri } from "./payto.js";
@@ -62,6 +63,7 @@ import {
CoinEnvelope,
DenomKeyType,
DenominationPubKey,
+ EddsaPrivateKeyString,
ExchangeAuditor,
ExchangeWireAccount,
InternationalizedString,
@@ -148,6 +150,27 @@ function codecForTombstoneIdStr(): Codec<TombstoneIdStr> {
};
}
+export function codecForCanonBaseUrl(): Codec<string> {
+ return {
+ decode(x: any, c?: Context): string {
+ if (typeof x === "string") {
+ const canon = canonicalizeBaseUrl(x);
+ if (x !== canon) {
+ throw new DecodingError(
+ `expected canonicalized base URL at ${renderContext(
+ c,
+ )} but got value '${x}'`,
+ );
+ }
+ return x;
+ }
+ throw new DecodingError(
+ `expected base URL at ${renderContext(c)} but got type ${typeof x}`,
+ );
+ },
+ };
+}
+
/**
* Response for the create reserve request to the wallet.
*/
@@ -293,15 +316,10 @@ interface GetPlanForPaymentRequest extends GetPlanToCompleteOperation {
maxDepositFee: AmountString;
}
-// interface GetPlanForTipRequest extends GetPlanForOperationBase {
-// type: TransactionType.Tip;
-// }
-// interface GetPlanForRefundRequest extends GetPlanForOperationBase {
-// type: TransactionType.Refund;
-// }
interface GetPlanForPullDebitRequest extends GetPlanToCompleteOperation {
type: TransactionType.PeerPullDebit;
}
+
interface GetPlanForPushCreditRequest extends GetPlanToCompleteOperation {
type: TransactionType.PeerPushCredit;
}
@@ -462,7 +480,7 @@ export interface GetCurrencySpecificationResponse {
export interface BuiltinExchange {
exchangeBaseUrl: string;
- currencyHint?: string;
+ currencyHint: string;
}
export interface PartialWalletRunConfig {
@@ -579,6 +597,11 @@ export enum CoinStatus {
Fresh = "fresh",
/**
+ * Coin was lost as the denomination is not usable anymore.
+ */
+ DenomLoss = "denom-loss",
+
+ /**
* Fresh, but currently marked as "suspended", thus won't be used
* for spending. Used for testing.
*/
@@ -739,71 +762,6 @@ export interface PrepareRefundResult {
info: OrderShortInfo;
}
-export interface PrepareTipResult {
- /**
- * Unique ID for the tip assigned by the wallet.
- * Typically different from the merchant-generated tip ID.
- *
- * @deprecated use transactionId instead
- */
- walletRewardId: string;
-
- /**
- * Tip transaction ID.
- */
- transactionId: TransactionIdStr;
-
- /**
- * Has the tip already been accepted?
- */
- accepted: boolean;
-
- /**
- * Amount that the merchant gave.
- */
- rewardAmountRaw: AmountString;
-
- /**
- * Amount that arrived at the wallet.
- * Might be lower than the raw amount due to fees.
- */
- rewardAmountEffective: AmountString;
-
- /**
- * Base URL of the merchant backend giving then tip.
- */
- merchantBaseUrl: string;
-
- /**
- * Base URL of the exchange that is used to withdraw the tip.
- * Determined by the merchant, the wallet/user has no choice here.
- */
- exchangeBaseUrl: string;
-
- /**
- * Time when the tip will expire. After it expired, it can't be picked
- * up anymore.
- */
- expirationTimestamp: TalerProtocolTimestamp;
-}
-
-export interface AcceptTipResponse {
- transactionId: TransactionIdStr;
- next_url?: string;
-}
-
-export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
- buildCodecForObject<PrepareTipResult>()
- .property("accepted", codecForBoolean())
- .property("rewardAmountRaw", codecForAmountString())
- .property("rewardAmountEffective", codecForAmountString())
- .property("exchangeBaseUrl", codecForString())
- .property("merchantBaseUrl", codecForString())
- .property("expirationTimestamp", codecForTimestamp)
- .property("walletRewardId", codecForString())
- .property("transactionId", codecForTransactionIdStr())
- .build("PrepareRewardResult");
-
export interface BenchmarkResult {
time: { [s: string]: number };
repetitions: number;
@@ -1057,7 +1015,7 @@ export interface TalerErrorDetail {
/**
* Minimal information needed about a planchet for unblinding a signature.
*
- * Can be a withdrawal/tipping/refresh planchet.
+ * Can be a withdrawal/refresh planchet.
*/
export interface PlanchetUnblindInfo {
denomPub: DenominationPubKey;
@@ -1392,7 +1350,7 @@ export interface ShortExchangeListItem {
export interface ExchangeListItem {
exchangeBaseUrl: string;
masterPub: string | undefined;
- currency: string | undefined;
+ currency: string;
paytoUris: string[];
tosStatus: ExchangeTosStatus;
exchangeEntryStatus: ExchangeEntryStatus;
@@ -1410,7 +1368,7 @@ export interface ExchangeListItem {
*/
noFees: boolean;
- scopeInfo: ScopeInfo | undefined;
+ scopeInfo: ScopeInfo;
lastUpdateTimestamp: TalerPreciseTimestamp | undefined;
@@ -1464,7 +1422,7 @@ export const codecForFeesByOperations = (): Codec<
export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
buildCodecForObject<ExchangeFullDetails>()
.property("currency", codecForString())
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("paytoUris", codecForList(codecForString()))
.property("auditors", codecForList(codecForExchangeAuditor()))
.property("wireInfo", codecForWireInfo())
@@ -1478,8 +1436,8 @@ export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
buildCodecForObject<ExchangeListItem>()
- .property("currency", codecOptional(codecForString()))
- .property("exchangeBaseUrl", codecForString())
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("masterPub", codecOptional(codecForString()))
.property("paytoUris", codecForList(codecForString()))
.property("tosStatus", codecForAny())
@@ -1583,6 +1541,8 @@ export interface DenomSelectionState {
totalCoinValue: AmountString;
totalWithdrawCost: AmountString;
selectedDenoms: DenomSelItem[];
+ earliestDepositExpiration: TalerProtocolTimestamp;
+ hasDenomWithAgeRestriction: boolean;
}
/**
@@ -1616,16 +1576,6 @@ export interface ExchangeWithdrawalDetails {
earliestDepositExpiration: TalerProtocolTimestamp;
/**
- * Number of currently offered denominations.
- */
- numOfferedDenoms: number;
-
- /**
- * Public keys of trusted auditors for the currency we're withdrawing.
- */
- trustedAuditorPubs: string[];
-
- /**
* Result of checking the wallet's version
* against the exchange's version.
*
@@ -1711,7 +1661,7 @@ export interface TestPayArgs {
export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
buildCodecForObject<TestPayArgs>()
- .property("merchantBaseUrl", codecForString())
+ .property("merchantBaseUrl", codecForCanonBaseUrl())
.property("merchantAuthToken", codecOptional(codecForString()))
.property("amount", codecForAmountString())
.property("summary", codecForString())
@@ -1729,12 +1679,12 @@ export interface IntegrationTestArgs {
export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
buildCodecForObject<IntegrationTestArgs>()
- .property("exchangeBaseUrl", codecForString())
- .property("merchantBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("merchantBaseUrl", codecForCanonBaseUrl())
.property("merchantAuthToken", codecOptional(codecForString()))
.property("amountToSpend", codecForAmountString())
.property("amountToWithdraw", codecForAmountString())
- .property("corebankApiBaseUrl", codecForString())
+ .property("corebankApiBaseUrl", codecForCanonBaseUrl())
.build("IntegrationTestArgs");
export interface IntegrationTestV2Args {
@@ -1746,10 +1696,10 @@ export interface IntegrationTestV2Args {
export const codecForIntegrationTestV2Args = (): Codec<IntegrationTestV2Args> =>
buildCodecForObject<IntegrationTestV2Args>()
- .property("exchangeBaseUrl", codecForString())
- .property("merchantBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("merchantBaseUrl", codecForCanonBaseUrl())
.property("merchantAuthToken", codecOptional(codecForString()))
- .property("corebankApiBaseUrl", codecForString())
+ .property("corebankApiBaseUrl", codecForCanonBaseUrl())
.build("IntegrationTestV2Args");
export interface GetExchangeEntryByUrlRequest {
@@ -1759,7 +1709,7 @@ export interface GetExchangeEntryByUrlRequest {
export const codecForGetExchangeEntryByUrlRequest =
(): Codec<GetExchangeEntryByUrlRequest> =>
buildCodecForObject<GetExchangeEntryByUrlRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.build("GetExchangeEntryByUrlRequest");
export type GetExchangeEntryByUrlResponse = ExchangeListItem;
@@ -1777,7 +1727,7 @@ export interface AddExchangeRequest {
export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
buildCodecForObject<AddExchangeRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("forceUpdate", codecOptional(codecForBoolean()))
.property("masterPub", codecOptional(codecForString()))
.build("AddExchangeRequest");
@@ -1790,7 +1740,7 @@ export interface UpdateExchangeEntryRequest {
export const codecForUpdateExchangeEntryRequest =
(): Codec<UpdateExchangeEntryRequest> =>
buildCodecForObject<UpdateExchangeEntryRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("force", codecOptional(codecForBoolean()))
.build("UpdateExchangeEntryRequest");
@@ -1801,7 +1751,7 @@ export interface GetExchangeResourcesRequest {
export const codecForGetExchangeResourcesRequest =
(): Codec<GetExchangeResourcesRequest> =>
buildCodecForObject<GetExchangeResourcesRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.build("GetExchangeResourcesRequest");
export interface GetExchangeResourcesResponse {
@@ -1815,7 +1765,7 @@ export interface DeleteExchangeRequest {
export const codecForDeleteExchangeRequest = (): Codec<DeleteExchangeRequest> =>
buildCodecForObject<DeleteExchangeRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("purge", codecOptional(codecForBoolean()))
.build("DeleteExchangeRequest");
@@ -1826,7 +1776,7 @@ export interface ForceExchangeUpdateRequest {
export const codecForForceExchangeUpdateRequest =
(): Codec<AddExchangeRequest> =>
buildCodecForObject<AddExchangeRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.build("AddExchangeRequest");
export interface GetExchangeTosRequest {
@@ -1837,7 +1787,7 @@ export interface GetExchangeTosRequest {
export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
buildCodecForObject<GetExchangeTosRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("acceptedFormat", codecOptional(codecForList(codecForString())))
.property("acceptLanguage", codecOptional(codecForString()))
.build("GetExchangeTosRequest");
@@ -1846,22 +1796,75 @@ export interface AcceptManualWithdrawalRequest {
exchangeBaseUrl: string;
amount: AmountString;
restrictAge?: number;
+
+ /**
+ * Instead of generating a fresh, random reserve key pair,
+ * use the provided reserve private key.
+ *
+ * Use with caution. Usage of this field may be restricted
+ * to developer mode.
+ */
+ forceReservePriv?: EddsaPrivateKeyString;
}
export const codecForAcceptManualWithdrawalRequest =
(): Codec<AcceptManualWithdrawalRequest> =>
buildCodecForObject<AcceptManualWithdrawalRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("amount", codecForAmountString())
.property("restrictAge", codecOptional(codecForNumber()))
+ .property("forceReservePriv", codecOptional(codecForString()))
.build("AcceptManualWithdrawalRequest");
export interface GetWithdrawalDetailsForAmountRequest {
exchangeBaseUrl: string;
amount: AmountString;
restrictAge?: number;
+
+ /**
+ * ID provided by the client to cancel the request.
+ *
+ * If the same request is made again with the same clientCancellationId,
+ * all previous requests are cancelled.
+ *
+ * The cancelled request will receive an error response with
+ * an error code that indicates the cancellation.
+ *
+ * The cancellation is best-effort, responses might still arrive.
+ */
+ clientCancellationId?: string;
+}
+
+export interface PrepareBankIntegratedWithdrawalRequest {
+ talerWithdrawUri: string;
+ exchangeBaseUrl: string;
+ forcedDenomSel?: ForcedDenomSel;
+ restrictAge?: number;
+}
+
+export const codecForPrepareBankIntegratedWithdrawalRequest =
+ (): Codec<PrepareBankIntegratedWithdrawalRequest> =>
+ buildCodecForObject<PrepareBankIntegratedWithdrawalRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("talerWithdrawUri", codecForString())
+ .property("forcedDenomSel", codecForAny())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .build("PrepareBankIntegratedWithdrawalRequest");
+
+export interface PrepareBankIntegratedWithdrawalResponse {
+ transactionId: string;
+}
+
+export interface ConfirmWithdrawalRequest {
+ transactionId: string;
}
+export const codecForConfirmWithdrawalRequestRequest =
+ (): Codec<ConfirmWithdrawalRequest> =>
+ buildCodecForObject<ConfirmWithdrawalRequest>()
+ .property("transactionId", codecForString())
+ .build("ConfirmWithdrawalRequest");
+
export interface AcceptBankIntegratedWithdrawalRequest {
talerWithdrawUri: string;
exchangeBaseUrl: string;
@@ -1872,7 +1875,7 @@ export interface AcceptBankIntegratedWithdrawalRequest {
export const codecForAcceptBankIntegratedWithdrawalRequest =
(): Codec<AcceptBankIntegratedWithdrawalRequest> =>
buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("talerWithdrawUri", codecForString())
.property("forcedDenomSel", codecForAny())
.property("restrictAge", codecOptional(codecForNumber()))
@@ -1881,9 +1884,10 @@ export const codecForAcceptBankIntegratedWithdrawalRequest =
export const codecForGetWithdrawalDetailsForAmountRequest =
(): Codec<GetWithdrawalDetailsForAmountRequest> =>
buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("amount", codecForAmountString())
.property("restrictAge", codecOptional(codecForNumber()))
+ .property("clientCancellationId", codecOptional(codecForString()))
.build("GetWithdrawalDetailsForAmountRequest");
export interface AcceptExchangeTosRequest {
@@ -1893,7 +1897,7 @@ export interface AcceptExchangeTosRequest {
export const codecForAcceptExchangeTosRequest =
(): Codec<AcceptExchangeTosRequest> =>
buildCodecForObject<AcceptExchangeTosRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.build("AcceptExchangeTosRequest");
export interface ForgetExchangeTosRequest {
@@ -1903,7 +1907,7 @@ export interface ForgetExchangeTosRequest {
export const codecForForgetExchangeTosRequest =
(): Codec<ForgetExchangeTosRequest> =>
buildCodecForObject<ForgetExchangeTosRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.build("ForgetExchangeTosRequest");
export interface AcceptRefundRequest {
@@ -1928,7 +1932,6 @@ export const codecForApplyRefundFromPurchaseIdRequest =
export interface GetWithdrawalDetailsForUriRequest {
talerWithdrawUri: string;
restrictAge?: number;
- notifyChangeFromPendingTimeoutMs?: number;
}
export const codecForGetWithdrawalDetailsForUri =
@@ -1936,10 +1939,6 @@ export const codecForGetWithdrawalDetailsForUri =
buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
.property("talerWithdrawUri", codecForString())
.property("restrictAge", codecOptional(codecForNumber()))
- .property(
- "notifyChangeFromPendingTimeoutMs",
- codecOptional(codecForNumber()),
- )
.build("GetWithdrawalDetailsForUriRequest");
export interface ListKnownBankAccountsRequest {
@@ -1985,13 +1984,16 @@ export const codecForAbortProposalRequest = (): Codec<AbortProposalRequest> =>
.build("AbortProposalRequest");
export interface GetContractTermsDetailsRequest {
- proposalId: string;
+ // @deprecated use transaction id
+ proposalId?: string;
+ transactionId?: string;
}
export const codecForGetContractTermsDetails =
(): Codec<GetContractTermsDetailsRequest> =>
buildCodecForObject<GetContractTermsDetailsRequest>()
- .property("proposalId", codecForString())
+ .property("proposalId", codecOptional(codecForString()))
+ .property("transactionId", codecOptional(codecForString()))
.build("GetContractTermsDetails");
export interface PreparePayRequest {
@@ -2009,7 +2011,7 @@ export interface SharePaymentRequest {
}
export const codecForSharePaymentRequest = (): Codec<SharePaymentRequest> =>
buildCodecForObject<SharePaymentRequest>()
- .property("merchantBaseUrl", codecForString())
+ .property("merchantBaseUrl", codecForCanonBaseUrl())
.property("orderId", codecForString())
.build("SharePaymentRequest");
@@ -2158,9 +2160,9 @@ export const codecForWithdrawTestBalance =
(): Codec<WithdrawTestBalanceRequest> =>
buildCodecForObject<WithdrawTestBalanceRequest>()
.property("amount", codecForAmountString())
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("forcedDenomSel", codecForAny())
- .property("corebankApiBaseUrl", codecForString())
+ .property("corebankApiBaseUrl", codecForCanonBaseUrl())
.build("WithdrawTestBalanceRequest");
export interface SetCoinSuspendedRequest {
@@ -2175,13 +2177,24 @@ export const codecForSetCoinSuspendedRequest =
.property("suspended", codecForBoolean())
.build("SetCoinSuspendedRequest");
+export interface RefreshCoinSpec {
+ coinPub: string;
+ amount?: AmountString;
+}
+
+export const codecForRefreshCoinSpec = (): Codec<RefreshCoinSpec> =>
+ buildCodecForObject<RefreshCoinSpec>()
+ .property("amount", codecForAmountString())
+ .property("coinPub", codecForString())
+ .build("ForceRefreshRequest");
+
export interface ForceRefreshRequest {
- coinPubList: string[];
+ refreshCoinSpecs: RefreshCoinSpec[];
}
export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
buildCodecForObject<ForceRefreshRequest>()
- .property("coinPubList", codecForList(codecForString()))
+ .property("refreshCoinSpecs", codecForList(codecForRefreshCoinSpec()))
.build("ForceRefreshRequest");
export interface PrepareRefundRequest {
@@ -2210,32 +2223,6 @@ export const codecForStartRefundQueryRequest =
.property("transactionId", codecForTransactionIdStr())
.build("StartRefundQueryRequest");
-export interface PrepareRewardRequest {
- talerRewardUri: string;
-}
-
-export const codecForPrepareRewardRequest = (): Codec<PrepareRewardRequest> =>
- buildCodecForObject<PrepareRewardRequest>()
- .property("talerRewardUri", codecForString())
- .build("PrepareRewardRequest");
-
-export interface AcceptRewardRequest {
- /**
- * @deprecated use transactionId
- */
- walletRewardId?: string;
- /**
- * it will be required when "walletRewardId" is removed
- */
- transactionId?: TransactionIdStr;
-}
-
-export const codecForAcceptTipRequest = (): Codec<AcceptRewardRequest> =>
- buildCodecForObject<AcceptRewardRequest>()
- .property("walletRewardId", codecOptional(codecForString()))
- .property("transactionId", codecOptional(codecForTransactionIdStr()))
- .build("AcceptRewardRequest");
-
export interface FailTransactionRequest {
transactionId: TransactionIdStr;
}
@@ -2353,7 +2340,7 @@ export const codecForWithdrawUriInfoResponse =
),
)
.property("amount", codecForAmountString())
- .property("defaultExchangeBaseUrl", codecOptional(codecForString()))
+ .property("defaultExchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
.property("possibleExchanges", codecForList(codecForExchangeListItem()))
.build("WithdrawUriInfoResponse");
@@ -2596,30 +2583,30 @@ export const codecForWithdrawFakebankRequest =
.build("WithdrawFakebankRequest");
export interface ActiveTask {
- id: string;
+ taskId: string;
transaction: TransactionIdStr | undefined;
firstTry: AbsoluteTime | undefined;
nextTry: AbsoluteTime | undefined;
- counter: number | undefined;
+ retryCounter: number | undefined;
lastError: TalerErrorDetail | undefined;
}
-export interface GetActiveTasks {
+export interface GetActiveTasksResponse {
tasks: ActiveTask[];
}
export const codecForActiveTask = (): Codec<ActiveTask> =>
buildCodecForObject<ActiveTask>()
- .property("id", codecForString())
+ .property("taskId", codecForString())
.property("transaction", codecOptional(codecForTransactionIdStr()))
- .property("counter", codecForNumber())
- .property("firstTry", codecForAbsoluteTime)
- .property("nextTry", codecForAbsoluteTime)
- .property("lastError", codecForTalerErrorDetail())
+ .property("retryCounter", codecOptional(codecForNumber()))
+ .property("firstTry", codecOptional(codecForAbsoluteTime))
+ .property("nextTry", codecOptional(codecForAbsoluteTime))
+ .property("lastError", codecOptional(codecForTalerErrorDetail()))
.build("ActiveTask");
-export const codecForGetActiveTasks = (): Codec<GetActiveTasks> =>
- buildCodecForObject<GetActiveTasks>()
+export const codecForGetActiveTasks = (): Codec<GetActiveTasksResponse> =>
+ buildCodecForObject<GetActiveTasksResponse>()
.property("tasks", codecForList(codecForActiveTask()))
.build("GetActiveTasks");
@@ -2657,8 +2644,16 @@ export interface TestPayResult {
}
export interface SelectedCoin {
+ denomPubHash: string;
coinPub: string;
contribution: AmountString;
+ exchangeBaseUrl: string;
+}
+
+export interface SelectedProspectiveCoin {
+ denomPubHash: string;
+ contribution: AmountString;
+ exchangeBaseUrl: string;
}
/**
@@ -2679,6 +2674,20 @@ export interface PayCoinSelection {
customerDepositFees: AmountString;
}
+export interface ProspectivePayCoinSelection {
+ prospectiveCoins: SelectedProspectiveCoin[];
+
+ /**
+ * How much of the wire fees is the customer paying?
+ */
+ customerWireFees: AmountString;
+
+ /**
+ * How much of the deposit fees is the customer paying?
+ */
+ customerDepositFees: AmountString;
+}
+
export interface CheckPeerPushDebitRequest {
/**
* Preferred exchange to use for the p2p payment.
@@ -2696,7 +2705,7 @@ export interface CheckPeerPushDebitRequest {
export const codecForCheckPeerPushDebitRequest =
(): Codec<CheckPeerPushDebitRequest> =>
buildCodecForObject<CheckPeerPushDebitRequest>()
- .property("exchangeBaseUrl", codecOptional(codecForString()))
+ .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
.property("amount", codecForAmountString())
.build("CheckPeerPushDebitRequest");
@@ -2835,7 +2844,7 @@ export const codecForPreparePeerPullPaymentRequest =
(): Codec<CheckPeerPullCreditRequest> =>
buildCodecForObject<CheckPeerPullCreditRequest>()
.property("amount", codecForAmountString())
- .property("exchangeBaseUrl", codecOptional(codecForString()))
+ .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
.build("CheckPeerPullCreditRequest");
export interface CheckPeerPullCreditResponse {
@@ -2858,7 +2867,7 @@ export const codecForInitiatePeerPullPaymentRequest =
(): Codec<InitiatePeerPullCreditRequest> =>
buildCodecForObject<InitiatePeerPullCreditRequest>()
.property("partialContractTerms", codecForPeerContractTerms())
- .property("exchangeBaseUrl", codecOptional(codecForString()))
+ .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
.build("InitiatePeerPullCreditRequest");
export interface InitiatePeerPullCreditResponse {
@@ -2873,6 +2882,20 @@ export interface InitiatePeerPullCreditResponse {
transactionId: TransactionIdStr;
}
+export interface CanonicalizeBaseUrlRequest {
+ url: string;
+}
+
+export const codecForCanonicalizeBaseUrlRequest =
+ (): Codec<CanonicalizeBaseUrlRequest> =>
+ buildCodecForObject<CanonicalizeBaseUrlRequest>()
+ .property("url", codecForString())
+ .build("CanonicalizeBaseUrlRequest");
+
+export interface CanonicalizeBaseUrlResponse {
+ url: string;
+}
+
export interface ValidateIbanRequest {
iban: string;
}
@@ -2987,6 +3010,18 @@ export interface TestingWaitTransactionRequest {
txState: TransactionState;
}
+export interface TestingGetReserveHistoryRequest {
+ reservePub: string;
+ exchangeBaseUrl: string;
+}
+
+export const codecForTestingGetReserveHistoryRequest =
+ (): Codec<TestingGetReserveHistoryRequest> =>
+ buildCodecForObject<TestingGetReserveHistoryRequest>()
+ .property("reservePub", codecForString())
+ .property("exchangeBaseUrl", codecForString())
+ .build("TestingGetReserveHistoryRequest");
+
export interface TestingGetDenomStatsRequest {
exchangeBaseUrl: string;
}
@@ -3000,7 +3035,7 @@ export interface TestingGetDenomStatsResponse {
export const codecForTestingGetDenomStatsRequest =
(): Codec<TestingGetDenomStatsRequest> =>
buildCodecForObject<TestingGetDenomStatsRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.build("TestingGetDenomStatsRequest");
export interface WithdrawalExchangeAccountDetails {
@@ -3120,7 +3155,7 @@ export const codecForAddGlobalCurrencyExchangeRequest =
(): Codec<AddGlobalCurrencyExchangeRequest> =>
buildCodecForObject<AddGlobalCurrencyExchangeRequest>()
.property("currency", codecForString())
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("exchangeMasterPub", codecForString())
.build("AddGlobalCurrencyExchangeRequest");
@@ -3134,7 +3169,7 @@ export const codecForRemoveGlobalCurrencyExchangeRequest =
(): Codec<RemoveGlobalCurrencyExchangeRequest> =>
buildCodecForObject<RemoveGlobalCurrencyExchangeRequest>()
.property("currency", codecForString())
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("exchangeMasterPub", codecForString())
.build("RemoveGlobalCurrencyExchangeRequest");
@@ -3148,7 +3183,7 @@ export const codecForAddGlobalCurrencyAuditorRequest =
(): Codec<AddGlobalCurrencyAuditorRequest> =>
buildCodecForObject<AddGlobalCurrencyAuditorRequest>()
.property("currency", codecForString())
- .property("auditorBaseUrl", codecForString())
+ .property("auditorBaseUrl", codecForCanonBaseUrl())
.property("auditorPub", codecForString())
.build("AddGlobalCurrencyAuditorRequest");
@@ -3162,20 +3197,10 @@ export const codecForRemoveGlobalCurrencyAuditorRequest =
(): Codec<RemoveGlobalCurrencyAuditorRequest> =>
buildCodecForObject<RemoveGlobalCurrencyAuditorRequest>()
.property("currency", codecForString())
- .property("auditorBaseUrl", codecForString())
+ .property("auditorBaseUrl", codecForCanonBaseUrl())
.property("auditorPub", codecForString())
.build("RemoveGlobalCurrencyAuditorRequest");
-export interface RetryLoopOpts {
- /**
- * Stop the retry loop when all lifeness-giving pending operations
- * are done.
- *
- * Defaults to false.
- */
- stopWhenDone?: boolean;
-}
-
/**
* Information about one provider.
*
diff --git a/packages/taler-util/src/whatwg-url.ts b/packages/taler-util/src/whatwg-url.ts
index 991528ae6..13abf5397 100644
--- a/packages/taler-util/src/whatwg-url.ts
+++ b/packages/taler-util/src/whatwg-url.ts
@@ -1908,15 +1908,22 @@ function parseURL(
}
export class URLImpl {
- constructor(url: string, base?: string) {
+ //Include URL type for "url" and "base" params.
+ constructor(url: string | URL, base?: string | URL) {
let parsedBase = null;
if (base !== undefined) {
+ if (base instanceof URL) {
+ base = base.href;
+ }
parsedBase = basicURLParse(base);
if (parsedBase === null) {
throw new TypeError(`Invalid base URL: ${base}`);
}
}
+ if (url instanceof URL) {
+ url = url.href;
+ }
const parsedURL = basicURLParse(url, { baseURL: parsedBase });
if (parsedURL === null) {
throw new TypeError(`Invalid URL: ${url}`);
diff --git a/packages/taler-wallet-cli/debian/changelog b/packages/taler-wallet-cli/debian/changelog
index dd4d4bb15..e136caa61 100644
--- a/packages/taler-wallet-cli/debian/changelog
+++ b/packages/taler-wallet-cli/debian/changelog
@@ -1,3 +1,27 @@
+taler-wallet-cli (0.10.7) unstable; urgency=low
+
+ * Release 0.10.7
+
+ -- Florian Dold <dold@taler.net> Mon, 22 Apr 2024 20:16:39 +0200
+
+taler-wallet-cli (0.10.6) unstable; urgency=low
+
+ * Release 0.10.6
+
+ -- Florian Dold <dold@taler.net> Wed, 10 Apr 2024 15:19:33 +0200
+
+taler-wallet-cli (0.10.5) unstable; urgency=low
+
+ * Release 0.10.5
+
+ -- Florian Dold <dold@taler.net> Tue, 09 Apr 2024 16:13:58 +0200
+
+taler-wallet-cli (0.10.4) unstable; urgency=low
+
+ * Release 0.10.4
+
+ -- Florian Dold <dold@taler.net> Tue, 09 Apr 2024 14:39:44 +0200
+
taler-wallet-cli (0.9.4a) unstable; urgency=low
* Release v0.9.4a.
diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json
index c93c9f8f7..922556749 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.10.0",
+ "version": "0.10.7",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
index 32b1eb901..b915de538 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -57,6 +57,7 @@ import { JsonMessage, runRpcServer } from "@gnu-taler/taler-util/twrpc";
import {
AccessStats,
createNativeWalletHost2,
+ nativeCrypto,
Wallet,
WalletApiOperation,
WalletCoreApiClient,
@@ -89,7 +90,8 @@ setUnhandledRejectionHandler((error: any) => {
processExit(1);
});
-const defaultWalletDbPath = pathHomedir() + "/" + ".talerwalletdb.json";
+const defaultWalletDbPath = pathHomedir() + "/" + ".talerwalletdb.sqlite3";
+const defaultWalletCoreSocket = pathHomedir() + "/" + ".wallet-core.sock";
function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
@@ -299,6 +301,17 @@ async function createLocalWallet(
}
}
+function writeObservabilityLog(notif: WalletNotification): void {
+ if (observabilityEventFile) {
+ switch (notif.type) {
+ case NotificationType.RequestObservabilityEvent:
+ case NotificationType.TaskObservabilityEvent:
+ fs.appendFileSync(observabilityEventFile, JSON.stringify(notif) + "\n");
+ break;
+ }
+ }
+}
+
async function withWallet<T>(
walletCliArgs: WalletCliArgsType,
f: (ctx: WalletContext) => Promise<T>,
@@ -307,17 +320,7 @@ async function withWallet<T>(
const onNotif = (notif: WalletNotification) => {
waiter.notify(notif);
- if (observabilityEventFile) {
- switch (notif.type) {
- case NotificationType.RequestObservabilityEvent:
- case NotificationType.TaskObservabilityEvent:
- fs.appendFileSync(
- observabilityEventFile,
- JSON.stringify(notif) + "\n",
- );
- break;
- }
- }
+ writeObservabilityLog(notif);
};
if (walletCliArgs.wallet.walletConnection) {
@@ -347,7 +350,7 @@ async function withWallet<T>(
},
};
const result = await f(ctx);
- wh.wallet.stop();
+ await wh.wallet.client.call(WalletApiOperation.Shutdown, {});
if (process.env.TALER_WALLET_DBSTATS) {
console.log("database stats:");
console.log(j2s(wh.getStats()));
@@ -356,23 +359,6 @@ async function withWallet<T>(
}
}
-/**
- * Run a function with a local wallet.
- *
- * Stops the wallet after the function is done.
- */
-async function withLocalWallet<T>(
- walletCliArgs: WalletCliArgsType,
- f: (w: { client: WalletCoreApiClient; ws: Wallet }) => Promise<T>,
-): Promise<T> {
- const wh = await createLocalWallet(walletCliArgs);
- const w = wh.wallet;
- const res = await f({ client: w.client, ws: w });
- logger.info("Work done, stopping wallet.");
- w.stop();
- return res;
-}
-
walletCli
.subcommand("balance", "balance", { help: "Show wallet balance." })
.flag("json", ["--json"], {
@@ -446,6 +432,7 @@ transactionsCli.action(async (args) => {
currency: args.transactions.currency,
search: args.transactions.search,
includeRefreshes: args.transactions.includeRefreshes,
+ sort: "stable-ascending",
},
);
console.log(JSON.stringify(pending, undefined, 2));
@@ -579,12 +566,8 @@ walletCli
help: "Run until no more work is left.",
})
.action(async (args) => {
- await withLocalWallet(args, async (wallet) => {
- logger.info("running until pending operations are finished");
- await wallet.ws.runTaskLoop({
- stopWhenDone: true,
- });
- wallet.ws.stop();
+ await withWallet(args, async (ctx) => {
+ await ctx.client.call(WalletApiOperation.TestingWaitTasksDone, {});
});
});
@@ -724,7 +707,7 @@ walletCli
break;
}
default:
- console.log(`URI type (${parsedTalerUri}) not handled`);
+ console.log(`URI type (${parsedTalerUri.type}) not handled`);
break;
}
return;
@@ -741,6 +724,7 @@ withdrawCli
.requiredOption("amount", ["--amount"], clk.AMOUNT, {
help: "Amount to withdraw",
})
+ .maybeOption("forcedReservePriv", ["--forced-reserve-priv"], clk.STRING, {})
.maybeOption("restrictAge", ["--restrict-age"], clk.INT)
.action(async (args) => {
await withWallet(args, async (wallet) => {
@@ -753,7 +737,7 @@ withdrawCli
exchangeBaseUrl: exchangeBaseUrl,
},
);
- const acct = d.paytoUris[0];
+ const acct = d.withdrawalAccountsList[0];
if (!acct) {
console.log("exchange has no accounts");
return;
@@ -764,10 +748,11 @@ withdrawCli
amount,
exchangeBaseUrl,
restrictAge: parseInt(String(args.withdrawManually.restrictAge), 10),
+ forceReservePriv: args.withdrawManually.forcedReservePriv,
},
);
const reservePub = resp.reservePub;
- const completePaytoUri = addPaytoQueryParams(acct, {
+ const completePaytoUri = addPaytoQueryParams(acct.paytoUri, {
amount: args.withdrawManually.amount,
message: `Taler top-up ${reservePub}`,
});
@@ -986,7 +971,7 @@ depositCli
.requiredArgument("amount", clk.AMOUNT)
.requiredArgument("targetPayto", clk.STRING)
.action(async (args) => {
- await withLocalWallet(args, async (wallet) => {
+ await withWallet(args, async (wallet) => {
const resp = await wallet.client.call(
WalletApiOperation.CreateDepositGroup,
{
@@ -1190,6 +1175,34 @@ const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
});
advancedCli
+ .subcommand("genReserve", "gen-reserve", {
+ help: "Generate a reserve key pair (not stored in the DB).",
+ })
+ .action(async (args) => {
+ const pair = await nativeCrypto.createEddsaKeypair({});
+ console.log(
+ j2s({
+ reservePub: pair.pub,
+ reservePriv: pair.priv,
+ }),
+ );
+ });
+
+advancedCli
+ .subcommand("tasks", "tasks", {
+ help: "Show active wallet-core tasks.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const tasks = await wallet.client.call(
+ WalletApiOperation.GetActiveTasks,
+ {},
+ );
+ console.log(j2s(tasks));
+ });
+ });
+
+advancedCli
.subcommand("sampleTransactions", "sample-transactions", {
help: "Print sample wallet-core transactions",
})
@@ -1202,25 +1215,34 @@ advancedCli
help: "Serve the wallet API via a unix domain socket.",
})
.requiredOption("unixPath", ["--unix-path"], clk.STRING, {
- default: "wallet-core.sock",
+ default: defaultWalletCoreSocket,
})
.flag("noInit", ["--no-init"], {
help: "Do not initialize the wallet. The client must send the initWallet message.",
})
.action(async (args) => {
- logger.info(`serving at ${args.serve.unixPath}`);
- const onNotif = (notif: WalletNotification) => {
- if (observabilityEventFile) {
- switch (notif.type) {
- case NotificationType.RequestObservabilityEvent:
- case NotificationType.TaskObservabilityEvent:
- fs.appendFileSync(
- observabilityEventFile,
- JSON.stringify(notif) + "\n",
- );
- break;
- }
+ const socketPath = args.serve.unixPath;
+ logger.info(`serving at ${socketPath}`);
+ let cleanupCalled = false;
+
+ const cleanupSocket = (signal: string, code: number) => {
+ if (cleanupCalled) {
+ return;
+ }
+ cleanupCalled = true;
+ try {
+ logger.info("cleaning up socket");
+ fs.unlinkSync(socketPath);
+ } catch (e) {
+ logger.warn(`unable to clean up socket: ${e}`);
}
+ process.exit(128 + code);
+ };
+ process.on("SIGTERM", cleanupSocket);
+ process.on("SIGINT", cleanupSocket);
+
+ const onNotif = (notif: WalletNotification) => {
+ writeObservabilityLog(notif);
};
const wh = await createLocalWallet(args, onNotif, args.serve.noInit);
const w = wh.wallet;
@@ -1316,10 +1338,8 @@ advancedCli
exchangeBaseUrl: "http://localhost:8081/",
merchantBaseUrl: "http://localhost:8083/",
});
- await wallet.runTaskLoop({
- stopWhenDone: true,
- });
- wallet.stop();
+ await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ await wallet.client.call(WalletApiOperation.Shutdown, {});
});
advancedCli
@@ -1536,7 +1556,11 @@ advancedCli
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.ForceRefresh, {
- coinPubList: [args.refresh.coinPub],
+ refreshCoinSpecs: [
+ {
+ coinPub: args.refresh.coinPub,
+ },
+ ],
});
});
});
diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json
index de58bb750..46b3cef4e 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.10.0",
+ "version": "0.10.7",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-core/src/attention.ts b/packages/taler-wallet-core/src/attention.ts
index 60d2117f1..7a52ceaa3 100644
--- a/packages/taler-wallet-core/src/attention.ts
+++ b/packages/taler-wallet-core/src/attention.ts
@@ -29,7 +29,7 @@ import {
UserAttentionsResponse,
} from "@gnu-taler/taler-util";
import { timestampPreciseFromDb, timestampPreciseToDb } from "./db.js";
-import { InternalWalletState, WalletExecutionContext } from "./wallet.js";
+import { WalletExecutionContext } from "./wallet.js";
const logger = new Logger("operations/attention.ts");
@@ -37,20 +37,23 @@ export async function getUserAttentionsUnreadCount(
wex: WalletExecutionContext,
req: UserAttentionsRequest,
): Promise<UserAttentionsCountResponse> {
- const total = await wex.db.runReadOnlyTx(["userAttention"], async (tx) => {
- let count = 0;
- await tx.userAttention.iter().forEach((x) => {
- if (
- req.priority !== undefined &&
- UserAttentionPriority[x.info.type] !== req.priority
- )
- return;
- if (x.read !== undefined) return;
- count++;
- });
+ const total = await wex.db.runReadOnlyTx(
+ { storeNames: ["userAttention"] },
+ async (tx) => {
+ let count = 0;
+ await tx.userAttention.iter().forEach((x) => {
+ if (
+ req.priority !== undefined &&
+ UserAttentionPriority[x.info.type] !== req.priority
+ )
+ return;
+ if (x.read !== undefined) return;
+ count++;
+ });
- return count;
- });
+ return count;
+ },
+ );
return { total };
}
@@ -59,30 +62,33 @@ export async function getUserAttentions(
wex: WalletExecutionContext,
req: UserAttentionsRequest,
): Promise<UserAttentionsResponse> {
- return await wex.db.runReadOnlyTx(["userAttention"], async (tx) => {
- const pending: UserAttentionUnreadList = [];
- await tx.userAttention.iter().forEach((x) => {
- if (
- req.priority !== undefined &&
- UserAttentionPriority[x.info.type] !== req.priority
- )
- return;
- pending.push({
- info: x.info,
- when: timestampPreciseFromDb(x.created),
- read: x.read !== undefined,
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["userAttention"] },
+ async (tx) => {
+ const pending: UserAttentionUnreadList = [];
+ await tx.userAttention.iter().forEach((x) => {
+ if (
+ req.priority !== undefined &&
+ UserAttentionPriority[x.info.type] !== req.priority
+ )
+ return;
+ pending.push({
+ info: x.info,
+ when: timestampPreciseFromDb(x.created),
+ read: x.read !== undefined,
+ });
});
- });
- return { pending };
- });
+ return { pending };
+ },
+ );
}
export async function markAttentionRequestAsRead(
wex: WalletExecutionContext,
req: UserAttentionByIdRequest,
): Promise<void> {
- await wex.db.runReadWriteTx(["userAttention"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["userAttention"] }, async (tx) => {
const ua = await tx.userAttention.get([req.entityId, req.type]);
if (!ua) throw Error("attention request not found");
tx.userAttention.put({
@@ -104,7 +110,7 @@ export async function addAttentionRequest(
info: AttentionInfo,
entityId: string,
): Promise<void> {
- await wex.db.runReadWriteTx(["userAttention"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["userAttention"] }, async (tx) => {
await tx.userAttention.put({
info,
entityId,
@@ -125,7 +131,7 @@ export async function removeAttentionRequest(
wex: WalletExecutionContext,
req: UserAttentionByIdRequest,
): Promise<void> {
- await wex.db.runReadWriteTx(["userAttention"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["userAttention"] }, async (tx) => {
const ua = await tx.userAttention.get([req.entityId, req.type]);
if (!ua) throw Error("attention request not found");
await tx.userAttention.delete([req.entityId, req.type]);
diff --git a/packages/taler-wallet-core/src/backup/index.ts b/packages/taler-wallet-core/src/backup/index.ts
index c32ed8b8c..15904b470 100644
--- a/packages/taler-wallet-core/src/backup/index.ts
+++ b/packages/taler-wallet-core/src/backup/index.ts
@@ -46,7 +46,6 @@ import {
buildCodecForUnion,
bytesToString,
canonicalJson,
- canonicalizeBaseUrl,
checkDbInvariant,
checkLogicInvariant,
codecForBoolean,
@@ -183,7 +182,7 @@ async function runBackupCycleForProvider(
args: BackupForProviderArgs,
): Promise<TaskRunResult> {
const provider = await wex.db.runReadOnlyTx(
- ["backupProviders"],
+ { storeNames: ["backupProviders"] },
async (tx) => {
return tx.backupProviders.get(args.backupProviderBaseUrl);
},
@@ -232,10 +231,10 @@ async function runBackupCycleForProvider(
headers: {
"content-type": "application/octet-stream",
"sync-signature": syncSigResp.sig,
- "if-none-match": newHash,
+ "if-none-match": JSON.stringify(newHash),
...(provider.lastBackupHash
? {
- "if-match": provider.lastBackupHash,
+ "if-match": JSON.stringify(provider.lastBackupHash),
}
: {}),
},
@@ -244,20 +243,23 @@ async function runBackupCycleForProvider(
logger.trace(`sync response status: ${resp.status}`);
if (resp.status === HttpStatusCode.NotModified) {
- await wex.db.runReadWriteTx(["backupProviders"], async (tx) => {
- const prov = await tx.backupProviders.get(provider.baseUrl);
- if (!prov) {
- return;
- }
- prov.lastBackupCycleTimestamp = timestampPreciseToDb(
- TalerPreciseTimestamp.now(),
- );
- prov.state = {
- tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()),
- };
- await tx.backupProviders.put(prov);
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
+ const prov = await tx.backupProviders.get(provider.baseUrl);
+ if (!prov) {
+ return;
+ }
+ prov.lastBackupCycleTimestamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ prov.state = {
+ tag: BackupProviderStateTag.Ready,
+ nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()),
+ };
+ await tx.backupProviders.put(prov);
+ },
+ );
removeAttentionRequest(wex, {
entityId: provider.baseUrl,
@@ -290,41 +292,47 @@ async function runBackupCycleForProvider(
if (res === undefined) {
//claimed
- await wex.db.runReadWriteTx(["backupProviders"], async (tx) => {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
+ const prov = await tx.backupProviders.get(provider.baseUrl);
+ if (!prov) {
+ logger.warn("backup provider not found anymore");
+ return;
+ }
+ prov.shouldRetryFreshProposal = true;
+ prov.state = {
+ tag: BackupProviderStateTag.Retrying,
+ };
+ await tx.backupProviders.put(prov);
+ },
+ );
+
+ throw Error("not implemented");
+ // return {
+ // type: TaskRunResultType.Pending,
+ // };
+ }
+ const result = res;
+
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
const prov = await tx.backupProviders.get(provider.baseUrl);
if (!prov) {
logger.warn("backup provider not found anymore");
return;
}
- prov.shouldRetryFreshProposal = true;
+ // const opId = TaskIdentifiers.forBackup(prov);
+ // await scheduleRetryInTx(ws, tx, opId);
+ prov.currentPaymentProposalId = result.proposalId;
+ prov.shouldRetryFreshProposal = false;
prov.state = {
tag: BackupProviderStateTag.Retrying,
};
await tx.backupProviders.put(prov);
- });
-
- throw Error("not implemented");
- // return {
- // type: TaskRunResultType.Pending,
- // };
- }
- const result = res;
-
- await wex.db.runReadWriteTx(["backupProviders"], async (tx) => {
- const prov = await tx.backupProviders.get(provider.baseUrl);
- if (!prov) {
- logger.warn("backup provider not found anymore");
- return;
- }
- // const opId = TaskIdentifiers.forBackup(prov);
- // await scheduleRetryInTx(ws, tx, opId);
- prov.currentPaymentProposalId = result.proposalId;
- prov.shouldRetryFreshProposal = false;
- prov.state = {
- tag: BackupProviderStateTag.Retrying,
- };
- await tx.backupProviders.put(prov);
- });
+ },
+ );
addAttentionRequest(
wex,
@@ -343,21 +351,24 @@ async function runBackupCycleForProvider(
}
if (resp.status === HttpStatusCode.NoContent) {
- await wex.db.runReadWriteTx(["backupProviders"], async (tx) => {
- const prov = await tx.backupProviders.get(provider.baseUrl);
- if (!prov) {
- return;
- }
- prov.lastBackupHash = encodeCrock(currentBackupHash);
- prov.lastBackupCycleTimestamp = timestampPreciseToDb(
- TalerPreciseTimestamp.now(),
- );
- prov.state = {
- tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()),
- };
- await tx.backupProviders.put(prov);
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
+ const prov = await tx.backupProviders.get(provider.baseUrl);
+ if (!prov) {
+ return;
+ }
+ prov.lastBackupHash = encodeCrock(currentBackupHash);
+ prov.lastBackupCycleTimestamp = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ prov.state = {
+ tag: BackupProviderStateTag.Ready,
+ nextBackupTimestamp: timestampPreciseToDb(getNextBackupTimestamp()),
+ };
+ await tx.backupProviders.put(prov);
+ },
+ );
removeAttentionRequest(wex, {
entityId: provider.baseUrl,
@@ -376,22 +387,25 @@ async function runBackupCycleForProvider(
// const blob = await decryptBackup(backupConfig, backupEnc);
// FIXME: Re-implement backup import with merging
// await importBackup(ws, blob, cryptoData);
- await wex.db.runReadWriteTx(["backupProviders"], async (tx) => {
- const prov = await tx.backupProviders.get(provider.baseUrl);
- if (!prov) {
- logger.warn("backup provider not found anymore");
- return;
- }
- prov.lastBackupHash = encodeCrock(hash(backupEnc));
- // FIXME: Allocate error code for this situation?
- // FIXME: Add operation retry record!
- const opId = TaskIdentifiers.forBackup(prov);
- //await scheduleRetryInTx(ws, tx, opId);
- prov.state = {
- tag: BackupProviderStateTag.Retrying,
- };
- await tx.backupProviders.put(prov);
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
+ const prov = await tx.backupProviders.get(provider.baseUrl);
+ if (!prov) {
+ logger.warn("backup provider not found anymore");
+ return;
+ }
+ prov.lastBackupHash = encodeCrock(hash(backupEnc));
+ // FIXME: Allocate error code for this situation?
+ // FIXME: Add operation retry record!
+ const opId = TaskIdentifiers.forBackup(prov);
+ //await scheduleRetryInTx(ws, tx, opId);
+ prov.state = {
+ tag: BackupProviderStateTag.Retrying,
+ };
+ await tx.backupProviders.put(prov);
+ },
+ );
logger.info("processed existing backup");
// Now upload our own, merged backup.
return await runBackupCycleForProvider(wex, args);
@@ -414,7 +428,7 @@ export async function processBackupForProvider(
backupProviderBaseUrl: string,
): Promise<TaskRunResult> {
const provider = await wex.db.runReadOnlyTx(
- ["backupProviders"],
+ { storeNames: ["backupProviders"] },
async (tx) => {
return await tx.backupProviders.get(backupProviderBaseUrl);
},
@@ -444,9 +458,12 @@ export async function removeBackupProvider(
wex: WalletExecutionContext,
req: RemoveBackupProviderRequest,
): Promise<void> {
- await wex.db.runReadWriteTx(["backupProviders"], async (tx) => {
- await tx.backupProviders.delete(req.provider);
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
+ await tx.backupProviders.delete(req.provider);
+ },
+ );
}
export interface RunBackupCycleRequest {
@@ -473,7 +490,7 @@ export async function runBackupCycle(
req: RunBackupCycleRequest,
): Promise<void> {
const providers = await wex.db.runReadOnlyTx(
- ["backupProviders"],
+ { storeNames: ["backupProviders"] },
async (tx) => {
if (req.providers) {
const rs = await Promise.all(
@@ -552,57 +569,65 @@ export async function addBackupProvider(
): Promise<AddBackupProviderResponse> {
logger.info(`adding backup provider ${j2s(req)}`);
await provideBackupState(wex);
- const canonUrl = canonicalizeBaseUrl(req.backupProviderBaseUrl);
- await wex.db.runReadWriteTx(["backupProviders"], async (tx) => {
- const oldProv = await tx.backupProviders.get(canonUrl);
- if (oldProv) {
- logger.info("old backup provider found");
+ const canonUrl = req.backupProviderBaseUrl;
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
+ const oldProv = await tx.backupProviders.get(canonUrl);
+ if (oldProv) {
+ logger.info("old backup provider found");
+ if (req.activate) {
+ oldProv.state = {
+ tag: BackupProviderStateTag.Ready,
+ nextBackupTimestamp: timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ ),
+ };
+ logger.info("setting existing backup provider to active");
+ await tx.backupProviders.put(oldProv);
+ }
+ return;
+ }
+ },
+ );
+ const termsUrl = new URL("config", canonUrl);
+ const resp = await wex.http.fetch(termsUrl.href);
+ const terms = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForSyncTermsOfServiceResponse(),
+ );
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders"] },
+ async (tx) => {
+ let state: BackupProviderState;
+ //FIXME: what is the difference provisional and ready?
if (req.activate) {
- oldProv.state = {
+ state = {
tag: BackupProviderStateTag.Ready,
nextBackupTimestamp: timestampPreciseToDb(
TalerPreciseTimestamp.now(),
),
};
- logger.info("setting existing backup provider to active");
- await tx.backupProviders.put(oldProv);
+ } else {
+ state = {
+ tag: BackupProviderStateTag.Provisional,
+ };
}
- return;
- }
- });
- const termsUrl = new URL("config", canonUrl);
- const resp = await wex.http.fetch(termsUrl.href);
- const terms = await readSuccessResponseJsonOrThrow(
- resp,
- codecForSyncTermsOfServiceResponse(),
+ await tx.backupProviders.put({
+ state,
+ name: req.name,
+ terms: {
+ annualFee: terms.annual_fee,
+ storageLimitInMegabytes: terms.storage_limit_in_megabytes,
+ supportedProtocolVersion: terms.version,
+ },
+ shouldRetryFreshProposal: false,
+ paymentProposalIds: [],
+ baseUrl: canonUrl,
+ uids: [encodeCrock(getRandomBytes(32))],
+ });
+ },
);
- await wex.db.runReadWriteTx(["backupProviders"], async (tx) => {
- let state: BackupProviderState;
- //FIXME: what is the difference provisional and ready?
- if (req.activate) {
- state = {
- tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
- };
- } else {
- state = {
- tag: BackupProviderStateTag.Provisional,
- };
- }
- await tx.backupProviders.put({
- state,
- name: req.name,
- terms: {
- annualFee: terms.annual_fee,
- storageLimitInMegabytes: terms.storage_limit_in_megabytes,
- supportedProtocolVersion: terms.version,
- },
- shouldRetryFreshProposal: false,
- paymentProposalIds: [],
- baseUrl: canonUrl,
- uids: [encodeCrock(getRandomBytes(32))],
- });
- });
return await runFirstBackupCycleForProvider(wex, {
backupProviderBaseUrl: canonUrl,
@@ -706,7 +731,7 @@ export async function getBackupInfo(
): Promise<BackupInfo> {
const backupConfig = await provideBackupState(wex);
const providerRecords = await wex.db.runReadOnlyTx(
- ["backupProviders", "operationRetries"],
+ { storeNames: ["backupProviders", "operationRetries"] },
async (tx) => {
return await tx.backupProviders.iter().mapAsync(async (bp) => {
const opId = TaskIdentifiers.forBackup(bp);
@@ -752,7 +777,7 @@ export async function getBackupRecovery(
): Promise<BackupRecovery> {
const bs = await provideBackupState(wex);
const providers = await wex.db.runReadOnlyTx(
- ["backupProviders"],
+ { storeNames: ["backupProviders"] },
async (tx) => {
return await tx.backupProviders.iter().toArray();
},
@@ -774,48 +799,51 @@ async function backupRecoveryTheirs(
wex: WalletExecutionContext,
br: BackupRecovery,
) {
- await wex.db.runReadWriteTx(["backupProviders", "config"], async (tx) => {
- let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
- ConfigRecordKey.WalletBackupState,
- );
- checkDbInvariant(!!backupStateEntry);
- checkDbInvariant(
- backupStateEntry.key === ConfigRecordKey.WalletBackupState,
- );
- backupStateEntry.value.lastBackupNonce = undefined;
- backupStateEntry.value.lastBackupTimestamp = undefined;
- backupStateEntry.value.lastBackupCheckTimestamp = undefined;
- backupStateEntry.value.lastBackupPlainHash = undefined;
- backupStateEntry.value.walletRootPriv = br.walletRootPriv;
- backupStateEntry.value.walletRootPub = encodeCrock(
- eddsaGetPublic(decodeCrock(br.walletRootPriv)),
- );
- await tx.config.put(backupStateEntry);
- for (const prov of br.providers) {
- const existingProv = await tx.backupProviders.get(prov.url);
- if (!existingProv) {
- await tx.backupProviders.put({
- baseUrl: prov.url,
- name: prov.name,
- paymentProposalIds: [],
- shouldRetryFreshProposal: false,
- state: {
- tag: BackupProviderStateTag.Ready,
- nextBackupTimestamp: timestampPreciseToDb(
- TalerPreciseTimestamp.now(),
- ),
- },
- uids: [encodeCrock(getRandomBytes(32))],
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["backupProviders", "config"] },
+ async (tx) => {
+ let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
+ ConfigRecordKey.WalletBackupState,
+ );
+ checkDbInvariant(!!backupStateEntry);
+ checkDbInvariant(
+ backupStateEntry.key === ConfigRecordKey.WalletBackupState,
+ );
+ backupStateEntry.value.lastBackupNonce = undefined;
+ backupStateEntry.value.lastBackupTimestamp = undefined;
+ backupStateEntry.value.lastBackupCheckTimestamp = undefined;
+ backupStateEntry.value.lastBackupPlainHash = undefined;
+ backupStateEntry.value.walletRootPriv = br.walletRootPriv;
+ backupStateEntry.value.walletRootPub = encodeCrock(
+ eddsaGetPublic(decodeCrock(br.walletRootPriv)),
+ );
+ await tx.config.put(backupStateEntry);
+ for (const prov of br.providers) {
+ const existingProv = await tx.backupProviders.get(prov.url);
+ if (!existingProv) {
+ await tx.backupProviders.put({
+ baseUrl: prov.url,
+ name: prov.name,
+ paymentProposalIds: [],
+ shouldRetryFreshProposal: false,
+ state: {
+ tag: BackupProviderStateTag.Ready,
+ nextBackupTimestamp: timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ ),
+ },
+ uids: [encodeCrock(getRandomBytes(32))],
+ });
+ }
}
- }
- const providers = await tx.backupProviders.iter().toArray();
- for (const prov of providers) {
- prov.lastBackupCycleTimestamp = undefined;
- prov.lastBackupHash = undefined;
- await tx.backupProviders.put(prov);
- }
- });
+ const providers = await tx.backupProviders.iter().toArray();
+ for (const prov of providers) {
+ prov.lastBackupCycleTimestamp = undefined;
+ prov.lastBackupHash = undefined;
+ await tx.backupProviders.put(prov);
+ }
+ },
+ );
}
async function backupRecoveryOurs(
@@ -831,7 +859,7 @@ export async function loadBackupRecovery(
): Promise<void> {
const bs = await provideBackupState(wex);
const providers = await wex.db.runReadOnlyTx(
- ["backupProviders"],
+ { storeNames: ["backupProviders"] },
async (tx) => {
return await tx.backupProviders.iter().toArray();
},
@@ -879,7 +907,7 @@ export async function provideBackupState(
wex: WalletExecutionContext,
): Promise<WalletBackupConfState> {
const bs: ConfigRecord | undefined = await wex.db.runReadOnlyTx(
- ["config"],
+ { storeNames: ["config"] },
async (tx) => {
return await tx.config.get(ConfigRecordKey.WalletBackupState);
},
@@ -895,7 +923,7 @@ export async function provideBackupState(
// FIXME: device ID should be configured when wallet is initialized
// and be based on hostname
const deviceId = `wallet-core-${encodeCrock(d)}`;
- return await wex.db.runReadWriteTx(["config"], async (tx) => {
+ return await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
ConfigRecordKey.WalletBackupState,
);
@@ -933,7 +961,7 @@ export async function setWalletDeviceId(
deviceId: string,
): Promise<void> {
await provideBackupState(wex);
- await wex.db.runReadWriteTx(["config"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
ConfigRecordKey.WalletBackupState,
);
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
index 67ce73d41..5a805b477 100644
--- a/packages/taler-wallet-core/src/balance.ts
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -357,6 +357,9 @@ export async function getBalancesInsideTransaction(
case WithdrawalGroupStatus.AbortedExchange:
case WithdrawalGroupStatus.FailedAbortingBank:
case WithdrawalGroupStatus.FailedBankAborted:
+ case WithdrawalGroupStatus.AbortedOtherWallet:
+ case WithdrawalGroupStatus.AbortedUserRefused:
+ case WithdrawalGroupStatus.DialogProposed:
case WithdrawalGroupStatus.Done:
// Does not count as pendingIncoming
return;
@@ -407,7 +410,7 @@ export async function getBalancesInsideTransaction(
switch (ppdRecord.status) {
case PeerPushDebitStatus.AbortingDeletePurse:
case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
- case PeerPushDebitStatus.SuspendedReady:
+ case PeerPushDebitStatus.PendingReady:
case PeerPushDebitStatus.SuspendedReady:
case PeerPushDebitStatus.PendingCreatePurse:
case PeerPushDebitStatus.SuspendedCreatePurse: {
@@ -472,19 +475,21 @@ export async function getBalances(
logger.trace("starting to compute balance");
const wbal = await wex.db.runReadWriteTx(
- [
- "coinAvailability",
- "coins",
- "depositGroups",
- "exchangeDetails",
- "exchanges",
- "globalCurrencyAuditors",
- "globalCurrencyExchanges",
- "purchases",
- "refreshGroups",
- "withdrawalGroups",
- "peerPushDebit",
- ],
+ {
+ storeNames: [
+ "coinAvailability",
+ "coins",
+ "depositGroups",
+ "exchangeDetails",
+ "exchanges",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "purchases",
+ "refreshGroups",
+ "withdrawalGroups",
+ "peerPushDebit",
+ ],
+ },
async (tx) => {
return getBalancesInsideTransaction(wex, tx);
},
@@ -557,13 +562,15 @@ export async function getPaymentBalanceDetails(
req: PaymentRestrictionsForBalance,
): Promise<PaymentBalanceDetails> {
return await wex.db.runReadOnlyTx(
- [
- "coinAvailability",
- "refreshGroups",
- "exchanges",
- "exchangeDetails",
- "denominations",
- ],
+ {
+ storeNames: [
+ "coinAvailability",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "denominations",
+ ],
+ },
async (tx) => {
return getPaymentBalanceDetailsInTx(wex, tx, req);
},
@@ -729,25 +736,28 @@ export async function getBalanceDetail(
): Promise<PaymentBalanceDetails> {
const exchanges: { exchangeBaseUrl: string; exchangePub: string }[] = [];
const wires = new Array<string>();
- await wex.db.runReadOnlyTx(["exchanges", "exchangeDetails"], async (tx) => {
- const allExchanges = await tx.exchanges.iter().toArray();
- for (const e of allExchanges) {
- const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
- if (!details || req.currency !== details.currency) {
- continue;
- }
- details.wireInfo.accounts.forEach((a) => {
- const payto = parsePaytoUri(a.payto_uri);
- if (payto && !wires.includes(payto.targetType)) {
- wires.push(payto.targetType);
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "exchangeDetails"] },
+ async (tx) => {
+ const allExchanges = await tx.exchanges.iter().toArray();
+ for (const e of allExchanges) {
+ const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
+ if (!details || req.currency !== details.currency) {
+ continue;
}
- });
- exchanges.push({
- exchangePub: details.masterPublicKey,
- exchangeBaseUrl: e.baseUrl,
- });
- }
- });
+ details.wireInfo.accounts.forEach((a) => {
+ const payto = parsePaytoUri(a.payto_uri);
+ if (payto && !wires.includes(payto.targetType)) {
+ wires.push(payto.targetType);
+ }
+ });
+ exchanges.push({
+ exchangePub: details.masterPublicKey,
+ exchangeBaseUrl: e.baseUrl,
+ });
+ }
+ },
+ );
return await getPaymentBalanceDetails(wex, {
currency: req.currency,
diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts
index 67cd08652..c7cb2857e 100644
--- a/packages/taler-wallet-core/src/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/coinSelection.test.ts
@@ -46,7 +46,6 @@ test("p2p: should select the coin", (t) => {
t.log(`tally before: ${j2s(tally)}`);
const coins = testing_selectGreedy(
{
- wireFeeAmortization: 1,
wireFeesPerExchange: {},
},
createCandidates([
@@ -71,8 +70,6 @@ test("p2p: should select the coin", (t) => {
denomPubHash: "hash0",
maxAge: 32,
contributions: [Amounts.parseOrThrow("LOCAL:2.1")],
- expireDeposit: inTheDistantFuture,
- expireWithdraw: inTheDistantFuture,
},
});
});
@@ -82,7 +79,6 @@ test("p2p: should select 3 coins", (t) => {
const tally = emptyTallyForPeerPayment(instructedAmount);
const coins = testing_selectGreedy(
{
- wireFeeAmortization: 1,
wireFeesPerExchange: {},
},
createCandidates([
@@ -106,8 +102,6 @@ test("p2p: should select 3 coins", (t) => {
Amounts.parseOrThrow("LOCAL:10"),
Amounts.parseOrThrow("LOCAL:0.3"),
],
- expireDeposit: inTheDistantFuture,
- expireWithdraw: inTheDistantFuture,
},
});
});
@@ -117,7 +111,6 @@ test("p2p: can't select since the instructed amount is too high", (t) => {
const tally = emptyTallyForPeerPayment(instructedAmount);
const coins = testing_selectGreedy(
{
- wireFeeAmortization: 1,
wireFeesPerExchange: {},
},
createCandidates([
@@ -148,7 +141,6 @@ test("pay: select one coin to pay with fee", (t) => {
} satisfies CoinSelectionTally;
const coins = testing_selectGreedy(
{
- wireFeeAmortization: 1,
wireFeesPerExchange: { "http://exchange.localhost/": exchangeWireFee },
},
createCandidates([
@@ -168,8 +160,6 @@ test("pay: select one coin to pay with fee", (t) => {
denomPubHash: "hash0",
maxAge: 32,
contributions: [Amounts.parseOrThrow("LOCAL:2.2")],
- expireDeposit: inTheDistantFuture,
- expireWithdraw: inTheDistantFuture,
},
});
@@ -282,7 +272,6 @@ test("p2p: regression STATER", (t) => {
const tally = emptyTallyForPeerPayment(instructedAmount);
const res = testing_selectGreedy(
{
- wireFeeAmortization: 1,
wireFeesPerExchange: {},
},
candidates as any,
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index 6e3ef5917..a60e41ecd 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -44,7 +44,9 @@ import {
parsePaytoUri,
PayCoinSelection,
PaymentInsufficientBalanceDetails,
+ ProspectivePayCoinSelection,
SelectedCoin,
+ SelectedProspectiveCoin,
strcmp,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
@@ -97,7 +99,6 @@ export interface CoinSelectionTally {
function tallyFees(
tally: CoinSelectionTally,
wireFeesPerExchange: Record<string, AmountJson>,
- wireFeeAmortization: number,
exchangeBaseUrl: string,
feeDeposit: AmountJson,
): void {
@@ -108,7 +109,7 @@ function tallyFees(
wireFeesPerExchange[exchangeBaseUrl] ?? Amounts.zeroOfCurrency(currency);
// The remaining, amortized amount needs to be paid by the
// wallet or covered by the deposit fee allowance.
- let wfRemaining = Amounts.divide(wf, wireFeeAmortization);
+ let wfRemaining = wf;
// This is the amount forgiven via the deposit fee allowance.
const wfDepositForgiven = Amounts.min(
tally.amountDepositFeeLimitRemaining,
@@ -158,8 +159,99 @@ export type SelectPayCoinsResult =
type: "failure";
insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
}
+ | { type: "prospective"; result: ProspectivePayCoinSelection }
| { type: "success"; coinSel: PayCoinSelection };
+async function internalSelectPayCoins(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "coins",
+ ]
+ >,
+ req: SelectPayCoinRequestNg,
+ includePendingCoins: boolean,
+): Promise<
+ | { sel: SelResult; coinRes: SelectedCoin[]; tally: CoinSelectionTally }
+ | undefined
+> {
+ const { contractTermsAmount, depositFeeLimit } = req;
+ const [candidateDenoms, wireFeesPerExchange] = await selectPayCandidates(
+ wex,
+ tx,
+ {
+ restrictExchanges: req.restrictExchanges,
+ instructedAmount: req.contractTermsAmount,
+ restrictWireMethod: req.restrictWireMethod,
+ depositPaytoUri: req.depositPaytoUri,
+ requiredMinimumAge: req.requiredMinimumAge,
+ includePendingCoins,
+ },
+ );
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(
+ `instructed amount: ${Amounts.stringify(req.contractTermsAmount)}`,
+ );
+ logger.trace(`wire fees per exchange: ${j2s(wireFeesPerExchange)}`);
+ logger.trace(`candidates: ${j2s(candidateDenoms)}`);
+ }
+
+ const coinRes: SelectedCoin[] = [];
+ const currency = contractTermsAmount.currency;
+
+ let tally: CoinSelectionTally = {
+ amountPayRemaining: contractTermsAmount,
+ amountDepositFeeLimitRemaining: depositFeeLimit,
+ customerDepositFees: Amounts.zeroOfCurrency(currency),
+ customerWireFees: Amounts.zeroOfCurrency(currency),
+ wireFeeCoveredForExchange: new Set(),
+ lastDepositFee: Amounts.zeroOfCurrency(currency),
+ };
+
+ await maybeRepairCoinSelection(
+ wex,
+ tx,
+ req.prevPayCoins ?? [],
+ coinRes,
+ tally,
+ {
+ wireFeesPerExchange: wireFeesPerExchange,
+ },
+ );
+
+ let selectedDenom: SelResult | undefined;
+ if (req.forcedSelection) {
+ selectedDenom = selectForced(req, candidateDenoms);
+ } else {
+ // FIXME: Here, we should select coins in a smarter way.
+ // Instead of always spending the next-largest coin,
+ // we should try to find the smallest coin that covers the
+ // amount.
+ selectedDenom = selectGreedy(
+ {
+ wireFeesPerExchange: wireFeesPerExchange,
+ },
+ candidateDenoms,
+ tally,
+ );
+ }
+
+ if (!selectedDenom) {
+ return undefined;
+ }
+ return {
+ sel: selectedDenom,
+ coinRes,
+ tally,
+ };
+}
+
/**
* Select coins to spend under the merchant's constraints.
*
@@ -171,85 +263,58 @@ export async function selectPayCoins(
wex: WalletExecutionContext,
req: SelectPayCoinRequestNg,
): Promise<SelectPayCoinsResult> {
- const { contractTermsAmount, depositFeeLimit } = req;
-
if (logger.shouldLogTrace()) {
logger.trace(`selecting coins for ${j2s(req)}`);
}
return await wex.db.runReadOnlyTx(
- [
- "coinAvailability",
- "denominations",
- "refreshGroups",
- "exchanges",
- "exchangeDetails",
- "coins",
- ],
+ {
+ storeNames: [
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "coins",
+ ],
+ },
async (tx) => {
- const [candidateDenoms, wireFeesPerExchange] = await selectPayCandidates(
- wex,
- tx,
- {
- restrictExchanges: req.restrictExchanges,
- instructedAmount: req.contractTermsAmount,
- restrictWireMethod: req.restrictWireMethod,
- depositPaytoUri: req.depositPaytoUri,
- requiredMinimumAge: req.requiredMinimumAge,
- },
- );
+ const materialAvSel = await internalSelectPayCoins(wex, tx, req, false);
- if (logger.shouldLogTrace()) {
- logger.trace(
- `instructed amount: ${Amounts.stringify(req.contractTermsAmount)}`,
+ if (!materialAvSel) {
+ const prospectiveAvSel = await internalSelectPayCoins(
+ wex,
+ tx,
+ req,
+ true,
);
- logger.trace(`wire fees per exchange: ${j2s(wireFeesPerExchange)}`);
- logger.trace(`candidates: ${j2s(candidateDenoms)}`);
- }
-
- const coinRes: SelectedCoin[] = [];
- const currency = contractTermsAmount.currency;
- let tally: CoinSelectionTally = {
- amountPayRemaining: contractTermsAmount,
- amountDepositFeeLimitRemaining: depositFeeLimit,
- customerDepositFees: Amounts.zeroOfCurrency(currency),
- customerWireFees: Amounts.zeroOfCurrency(currency),
- wireFeeCoveredForExchange: new Set(),
- lastDepositFee: Amounts.zeroOfCurrency(currency),
- };
-
- await maybeRepairCoinSelection(
- wex,
- tx,
- req.prevPayCoins ?? [],
- coinRes,
- tally,
- {
- wireFeeAmortization: req.wireFeeAmortization,
- wireFeesPerExchange: wireFeesPerExchange,
- },
- );
-
- let selectedDenom: SelResult | undefined;
- if (req.forcedSelection) {
- selectedDenom = selectForced(req, candidateDenoms);
- } else {
- // FIXME: Here, we should select coins in a smarter way.
- // Instead of always spending the next-largest coin,
- // we should try to find the smallest coin that covers the
- // amount.
- selectedDenom = selectGreedy(
- {
- wireFeeAmortization: req.wireFeeAmortization,
- wireFeesPerExchange: wireFeesPerExchange,
- },
- candidateDenoms,
- tally,
- );
- }
+ if (prospectiveAvSel) {
+ const prospectiveCoins: SelectedProspectiveCoin[] = [];
+ for (const avKey of Object.keys(prospectiveAvSel.sel)) {
+ const mySel = prospectiveAvSel.sel[avKey];
+ for (const contrib of mySel.contributions) {
+ prospectiveCoins.push({
+ denomPubHash: mySel.denomPubHash,
+ contribution: Amounts.stringify(contrib),
+ exchangeBaseUrl: mySel.exchangeBaseUrl,
+ });
+ }
+ }
+ return {
+ type: "prospective",
+ result: {
+ prospectiveCoins,
+ customerDepositFees: Amounts.stringify(
+ prospectiveAvSel.tally.customerDepositFees,
+ ),
+ customerWireFees: Amounts.stringify(
+ prospectiveAvSel.tally.customerWireFees,
+ ),
+ },
+ } satisfies SelectPayCoinsResult;
+ }
- if (!selectedDenom) {
return {
type: "failure",
insufficientBalanceDetails: await reportInsufficientBalanceDetails(
@@ -268,9 +333,9 @@ export async function selectPayCoins(
const coinSel = await assembleSelectPayCoinsSuccessResult(
tx,
- selectedDenom,
- coinRes,
- tally,
+ materialAvSel.sel,
+ materialAvSel.coinRes,
+ materialAvSel.tally,
);
if (logger.shouldLogTrace()) {
@@ -292,7 +357,6 @@ async function maybeRepairCoinSelection(
coinRes: SelectedCoin[],
tally: CoinSelectionTally,
feeInfo: {
- wireFeeAmortization: number;
wireFeesPerExchange: Record<string, AmountJson>;
},
): Promise<void> {
@@ -314,7 +378,6 @@ async function maybeRepairCoinSelection(
tallyFees(
tally,
feeInfo.wireFeesPerExchange,
- feeInfo.wireFeeAmortization,
coin.exchangeBaseUrl,
Amounts.parseOrThrow(denom.feeDeposit),
);
@@ -324,12 +387,18 @@ async function maybeRepairCoinSelection(
).amount;
coinRes.push({
+ exchangeBaseUrl: coin.exchangeBaseUrl,
+ denomPubHash: coin.denomPubHash,
coinPub: prev.coinPub,
contribution: Amounts.stringify(prev.contribution),
});
}
}
+/**
+ * Returns undefined if the success response could not be assembled,
+ * as not enough coins are actually available.
+ */
async function assembleSelectPayCoinsSuccessResult(
tx: WalletDbReadOnlyTransaction<["coins"]>,
finalSel: SelResult,
@@ -359,8 +428,10 @@ async function assembleSelectPayCoinsSuccessResult(
for (let i = 0; i < selInfo.contributions.length; i++) {
coinRes.push({
+ denomPubHash: coins[i].denomPubHash,
coinPub: coins[i].coinPub,
contribution: Amounts.stringify(selInfo.contributions[i]),
+ exchangeBaseUrl: coins[i].exchangeBaseUrl,
});
}
}
@@ -502,7 +573,6 @@ export function testing_selectGreedy(
}
export interface SelectGreedyRequest {
- wireFeeAmortization: number;
wireFeesPerExchange: Record<string, AmountJson>;
}
@@ -511,7 +581,6 @@ function selectGreedy(
candidateDenoms: AvailableDenom[],
tally: CoinSelectionTally,
): SelResult | undefined {
- const { wireFeeAmortization } = req;
const selectedDenom: SelResult = {};
for (const denom of candidateDenoms) {
const contributions: AmountJson[] = [];
@@ -531,7 +600,6 @@ function selectGreedy(
tallyFees(
tally,
req.wireFeesPerExchange,
- wireFeeAmortization,
denom.exchangeBaseUrl,
Amounts.parseOrThrow(denom.feeDeposit),
);
@@ -644,7 +712,6 @@ export interface SelectPayCoinRequestNg {
restrictWireMethod: string;
contractTermsAmount: AmountJson;
depositFeeLimit: AmountJson;
- wireFeeAmortization: number;
prevPayCoins?: PreviousPayCoins;
requiredMinimumAge?: number;
forcedSelection?: ForcedCoinSel;
@@ -745,6 +812,13 @@ interface SelectPayCandidatesRequest {
depositPaytoUri?: string;
restrictExchanges: ExchangeRestrictionSpec | undefined;
requiredMinimumAge?: number;
+
+ /**
+ * If set to true, the coin selection will also use coins that are not
+ * materially available yet, but that are expected to become available
+ * as the output of a refresh operation.
+ */
+ includePendingCoins: boolean;
}
async function selectPayCandidates(
@@ -845,9 +919,13 @@ async function selectPayCandidates(
continue;
}
numUsable++;
+ let numAvailable = coinAvail.freshCoinCount ?? 0;
+ if (req.includePendingCoins) {
+ numAvailable += coinAvail.pendingRefreshOutputCount ?? 0;
+ }
denoms.push({
...DenominationRecord.toDenomInfo(denom),
- numAvailable: coinAvail.freshCoinCount ?? 0,
+ numAvailable,
maxAge: coinAvail.maxAge,
});
}
@@ -886,8 +964,23 @@ export interface PeerCoinSelectionDetails {
maxExpirationDate: TalerProtocolTimestamp;
}
+export interface ProspectivePeerCoinSelectionDetails {
+ exchangeBaseUrl: string;
+
+ prospectiveCoins: SelectedProspectiveCoin[];
+
+ /**
+ * How much of the deposit fees is the customer paying?
+ */
+ depositFees: AmountJson;
+
+ maxExpirationDate: TalerProtocolTimestamp;
+}
+
export type SelectPeerCoinsResult =
| { type: "success"; result: PeerCoinSelectionDetails }
+ // Successful, but using coins that are not materially available yet.
+ | { type: "prospective"; result: ProspectivePeerCoinSelectionDetails }
| {
type: "failure";
insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
@@ -968,6 +1061,75 @@ function getGlobalFees(
return undefined;
}
+async function internalSelectPeerCoins(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchangeDetails",
+ ]
+ >,
+ req: PeerCoinSelectionRequest,
+ exch: ExchangeWireDetails,
+ includePendingCoins: boolean,
+): Promise<
+ | { sel: SelResult; tally: CoinSelectionTally; resCoins: SelectedCoin[] }
+ | undefined
+> {
+ const candidatesRes = await selectPayCandidates(wex, tx, {
+ instructedAmount: req.instructedAmount,
+ restrictExchanges: {
+ auditors: [],
+ exchanges: [
+ {
+ exchangeBaseUrl: exch.exchangeBaseUrl,
+ exchangePub: exch.masterPublicKey,
+ },
+ ],
+ },
+ restrictWireMethod: undefined,
+ includePendingCoins,
+ });
+ const candidates = candidatesRes[0];
+ if (logger.shouldLogTrace()) {
+ logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
+ }
+ const tally = emptyTallyForPeerPayment(req.instructedAmount);
+ const resCoins: SelectedCoin[] = [];
+
+ await maybeRepairCoinSelection(wex, tx, req.repair ?? [], resCoins, tally, {
+ wireFeesPerExchange: {},
+ });
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`candidates: ${j2s(candidates)}`);
+ logger.trace(`instructedAmount: ${j2s(req.instructedAmount)}`);
+ logger.trace(`tally: ${j2s(tally)}`);
+ }
+
+ const selRes = selectGreedy(
+ {
+ wireFeesPerExchange: {},
+ },
+ candidates,
+ tally,
+ );
+ if (!selRes) {
+ return undefined;
+ }
+
+ return {
+ sel: selRes,
+ tally,
+ resCoins,
+ };
+}
+
export async function selectPeerCoins(
wex: WalletExecutionContext,
req: PeerCoinSelectionRequest,
@@ -980,15 +1142,17 @@ export async function selectPeerCoins(
}
return await wex.db.runReadWriteTx(
- [
- "exchanges",
- "contractTerms",
- "coins",
- "coinAvailability",
- "denominations",
- "refreshGroups",
- "exchangeDetails",
- ],
+ {
+ storeNames: [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchangeDetails",
+ ],
+ },
async (tx): Promise<SelectPeerCoinsResult> => {
const exchanges = await tx.exchanges.iter().toArray();
const currency = Amounts.currencyOf(instructedAmount);
@@ -1004,65 +1168,63 @@ export async function selectPeerCoins(
if (!globalFees) {
continue;
}
- const candidatesRes = await selectPayCandidates(wex, tx, {
- instructedAmount,
- restrictExchanges: {
- auditors: [],
- exchanges: [
- {
- exchangeBaseUrl: exch.baseUrl,
- exchangePub: exch.detailsPointer.masterPublicKey,
- },
- ],
- },
- restrictWireMethod: undefined,
- });
- const candidates = candidatesRes[0];
- if (logger.shouldLogTrace()) {
- logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
- }
- const tally = emptyTallyForPeerPayment(req.instructedAmount);
- const resCoins: SelectedCoin[] = [];
- await maybeRepairCoinSelection(
+ const avRes = await internalSelectPeerCoins(
wex,
tx,
- req.repair ?? [],
- resCoins,
- tally,
- {
- wireFeeAmortization: 1,
- wireFeesPerExchange: {},
- },
+ req,
+ exchWire,
+ false,
);
- if (logger.shouldLogTrace()) {
- logger.trace(`candidates: ${j2s(candidates)}`);
- logger.trace(`instructedAmount: ${j2s(instructedAmount)}`);
- logger.trace(`tally: ${j2s(tally)}`);
- }
-
- const selectedDenom = selectGreedy(
- {
- wireFeeAmortization: 1,
- wireFeesPerExchange: {},
- },
- candidates,
- tally,
- );
-
- if (selectedDenom) {
+ if (!avRes) {
+ // Try to see if we can do a prospective selection
+ const prospectiveAvRes = await internalSelectPeerCoins(
+ wex,
+ tx,
+ req,
+ exchWire,
+ true,
+ );
+ if (prospectiveAvRes) {
+ const prospectiveCoins: SelectedProspectiveCoin[] = [];
+ for (const avKey of Object.keys(prospectiveAvRes.sel)) {
+ const mySel = prospectiveAvRes.sel[avKey];
+ for (const contrib of mySel.contributions) {
+ prospectiveCoins.push({
+ denomPubHash: mySel.denomPubHash,
+ contribution: Amounts.stringify(contrib),
+ exchangeBaseUrl: mySel.exchangeBaseUrl,
+ });
+ }
+ }
+ const maxExpirationDate = await computeCoinSelMaxExpirationDate(
+ wex,
+ tx,
+ prospectiveAvRes.sel,
+ );
+ return {
+ type: "prospective",
+ result: {
+ prospectiveCoins,
+ depositFees: prospectiveAvRes.tally.customerDepositFees,
+ exchangeBaseUrl: exch.baseUrl,
+ maxExpirationDate,
+ },
+ };
+ }
+ } else if (avRes) {
const r = await assembleSelectPayCoinsSuccessResult(
tx,
- selectedDenom,
- resCoins,
- tally,
+ avRes.sel,
+ avRes.resCoins,
+ avRes.tally,
);
const maxExpirationDate = await computeCoinSelMaxExpirationDate(
wex,
tx,
- selectedDenom,
+ avRes.sel,
);
return {
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
index eb06b8eb0..edaba5ba4 100644
--- a/packages/taler-wallet-core/src/common.ts
+++ b/packages/taler-wallet-core/src/common.ts
@@ -21,6 +21,7 @@ import {
AbsoluteTime,
AmountJson,
Amounts,
+ AsyncFlag,
CoinRefreshRequest,
CoinStatus,
Duration,
@@ -35,6 +36,7 @@ import {
TalerProtocolTimestamp,
TombstoneIdStr,
TransactionIdStr,
+ WalletNotification,
assertUnreachable,
checkDbInvariant,
checkLogicInvariant,
@@ -145,7 +147,13 @@ export async function makeCoinAvailable(
export async function spendCoins(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
- ["coins", "coinAvailability", "refreshGroups", "denominations"]
+ [
+ "coins",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ ]
>,
csi: CoinsSpendInfo,
): Promise<void> {
@@ -275,6 +283,8 @@ export function getExchangeUpdateStatusFromRecord(
return ExchangeUpdateStatus.ReadyUpdate;
case ExchangeEntryDbUpdateStatus.Suspended:
return ExchangeUpdateStatus.Suspended;
+ default:
+ assertUnreachable(r.updateStatus);
}
}
@@ -288,6 +298,8 @@ export function getExchangeEntryStatusFromRecord(
return ExchangeEntryStatus.Preset;
case ExchangeEntryDbRecordStatus.Used:
return ExchangeEntryStatus.Used;
+ default:
+ assertUnreachable(r.entryStatus);
}
}
@@ -480,25 +492,28 @@ function updateTimeout(
r.nextRetry = timestampPreciseToDb(TalerPreciseTimestamp.fromMilliseconds(t));
}
-export namespace DbRetryInfo {
- export function getDuration(
- r: DbRetryInfo | undefined,
- p: RetryPolicy = defaultRetryPolicy,
- ): Duration {
- if (!r) {
- // If we don't have any retry info, run immediately.
- return { d_ms: 0 };
- }
- if (p.backoffDelta.d_ms === "forever") {
- return { d_ms: "forever" };
- }
- const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
- return {
- d_ms:
- p.maxTimeout.d_ms === "forever" ? t : Math.min(p.maxTimeout.d_ms, t),
- };
+export function computeDbBackoff(retryCounter: number): DbPreciseTimestamp {
+ const now = AbsoluteTime.now();
+ if (now.t_ms === "never") {
+ throw Error("assertion failed");
+ }
+ const p = defaultRetryPolicy;
+ if (p.backoffDelta.d_ms === "forever") {
+ throw Error("assertion failed");
}
+ const nextIncrement =
+ p.backoffDelta.d_ms * Math.pow(p.backoffBase, retryCounter);
+
+ const t =
+ now.t_ms +
+ (p.maxTimeout.d_ms === "forever"
+ ? nextIncrement
+ : Math.min(p.maxTimeout.d_ms, nextIncrement));
+ return timestampPreciseToDb(TalerPreciseTimestamp.fromMilliseconds(t));
+}
+
+export namespace DbRetryInfo {
export function reset(p: RetryPolicy = defaultRetryPolicy): DbRetryInfo {
const now = TalerPreciseTimestamp.now();
const info: DbRetryInfo = {
@@ -756,3 +771,53 @@ export enum PendingTaskType {
declare const __taskIdStr: unique symbol;
export type TaskIdStr = string & { [__taskIdStr]: true };
+
+/**
+ * Wait until the wallet is in a particular state.
+ *
+ * Two functions must be provided:
+ * 1. checkState, which checks if the wallet is in the
+ * desired state.
+ * 2. filterNotification, which checks whether a notification
+ * might have lead to a state change.
+ */
+export async function genericWaitForState(
+ wex: WalletExecutionContext,
+ args: {
+ checkState: () => Promise<boolean>;
+ filterNotification: (notif: WalletNotification) => boolean;
+ },
+): Promise<void> {
+ await wex.taskScheduler.ensureRunning();
+
+ // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
+ const flag = new AsyncFlag();
+ // Raise purchaseNotifFlag whenever we get a notification
+ // about our refresh.
+ const cancelNotif = wex.ws.addNotificationListener((notif) => {
+ if (args.filterNotification(notif)) {
+ flag.raise();
+ }
+ });
+ const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => {
+ cancelNotif();
+ flag.raise();
+ });
+
+ try {
+ while (true) {
+ if (wex.cancellationToken.isCancelled) {
+ throw Error("cancelled");
+ }
+ if (await args.checkState()) {
+ return;
+ }
+ // Wait for the next transition
+ await flag.wait();
+ flag.reset();
+ }
+ } catch (e) {
+ unregisterOnCancelled();
+ cancelNotif();
+ }
+}
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index 77ee65e52..2a2958a71 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -214,6 +214,10 @@ export interface TalerCryptoInterface {
signPurseCreation(req: SignPurseCreationRequest): Promise<EddsaSigningResult>;
+ signReserveHistoryReq(
+ req: SignReserveHistoryReqRequest,
+ ): Promise<SignReserveHistoryReqResponse>;
+
signPurseDeposits(
req: SignPurseDepositsRequest,
): Promise<SignPurseDepositsResponse>;
@@ -438,6 +442,11 @@ export const nullCrypto: TalerCryptoInterface = {
): Promise<SignCoinHistoryResponse> {
throw new Error("Function not implemented.");
},
+ signReserveHistoryReq: function (
+ req: SignReserveHistoryReqRequest,
+ ): Promise<SignReserveHistoryReqResponse> {
+ throw new Error("Function not implemented.");
+ },
};
export type WithArg<X> = X extends (req: infer T) => infer R
@@ -475,6 +484,15 @@ export interface SignPurseCreationRequest {
minAge: number;
}
+export interface SignReserveHistoryReqRequest {
+ reservePriv: string;
+ startOffset: number;
+}
+
+export interface SignReserveHistoryReqResponse {
+ sig: string;
+}
+
export interface SpendCoinDetails {
coinPub: string;
coinPriv: string;
@@ -1468,15 +1486,12 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const hExchangeBaseUrl = hash(stringToBytes(req.exchangeBaseUrl + "\0"));
const deposits: PurseDeposit[] = [];
for (const c of req.coins) {
- let haveAch: boolean;
let maybeAch: Uint8Array;
if (c.ageCommitmentProof) {
- haveAch = true;
maybeAch = decodeCrock(
AgeRestriction.hashCommitment(c.ageCommitmentProof.commitment),
);
} else {
- haveAch = false;
maybeAch = new Uint8Array(32);
}
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT)
@@ -1733,6 +1748,23 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
sig: sigResp.sig,
};
},
+ async signReserveHistoryReq(
+ tci: TalerCryptoInterfaceR,
+ req: SignReserveHistoryReqRequest,
+ ): Promise<SignReserveHistoryReqResponse> {
+ const reserveHistoryBlob = buildSigPS(
+ TalerSignaturePurpose.WALLET_RESERVE_HISTORY,
+ )
+ .put(bufferForUint64(req.startOffset))
+ .build();
+ const sigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(reserveHistoryBlob),
+ priv: req.reservePriv,
+ });
+ return {
+ sig: sigResp.sig,
+ };
+ },
};
export interface EddsaSignRequest {
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 98390805b..b75e48c39 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -34,11 +34,13 @@ import {
Amounts,
AttentionInfo,
BackupProviderTerms,
+ CancellationToken,
Codec,
CoinEnvelope,
CoinPublicKeyString,
CoinRefreshRequest,
CoinStatus,
+ DenomLossEventType,
DenomSelectionState,
DenominationInfo,
DenominationPubKey,
@@ -149,7 +151,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
* backwards-compatible way or object stores and indices
* are added.
*/
-export const WALLET_DB_MINOR_VERSION = 8;
+export const WALLET_DB_MINOR_VERSION = 10;
declare const symDbProtocolTimestamp: unique symbol;
@@ -296,6 +298,11 @@ export enum WithdrawalGroupStatus {
SuspendedReady = 0x0110_0004,
/**
+ * Proposed to the user, has can choose to accept/refuse.
+ */
+ DialogProposed = 0x0101_0000,
+
+ /**
* We are telling the bank that we don't want to complete
* the withdrawal!
*/
@@ -336,6 +343,21 @@ export enum WithdrawalGroupStatus {
AbortedExchange = 0x0503_0001,
AbortedBank = 0x0503_0002,
+
+ /**
+ * User didn't refused the withdrawal.
+ */
+ AbortedUserRefused = 0x0503_0003,
+
+ /**
+ * Another wallet confirmed the withdrawal
+ * (by POSTing the reserve pub to the bank)
+ * before we had the chance.
+ *
+ * In this situation, we'll let the other wallet continue
+ * and give up ourselves.
+ */
+ AbortedOtherWallet = 0x0503_0004,
}
/**
@@ -673,6 +695,8 @@ export interface ExchangeEntryRecord {
*/
nextUpdateStamp: DbPreciseTimestamp;
+ updateRetryCounter?: number;
+
lastKeysEtag: string | undefined;
/**
@@ -1225,9 +1249,15 @@ export interface DbCoinSelection {
}
export interface PurchasePayInfo {
- payCoinSelection: DbCoinSelection;
+ /**
+ * Undefined if payment is blocked by a pending refund.
+ */
+ payCoinSelection?: DbCoinSelection;
+ /**
+ * Undefined if payment is blocked by a pending refund.
+ */
+ payCoinSelectionUid?: string;
totalPayCost: AmountString;
- payCoinSelectionUid: string;
}
/**
@@ -1366,7 +1396,6 @@ export type ConfigRecord =
value: WalletBackupConfState;
}
| { key: ConfigRecordKey.CurrencyDefaultsApplied; value: boolean }
- | { key: ConfigRecordKey.DevMode; value: boolean }
| { key: ConfigRecordKey.TestLoopTx; value: number }
| { key: ConfigRecordKey.LastInitInfo; value: DbProtocolTimestamp };
@@ -1786,9 +1815,9 @@ export interface DepositGroupRecord {
contractTermsHash: string;
- payCoinSelection: DbCoinSelection;
+ payCoinSelection?: DbCoinSelection;
- payCoinSelectionUid: string;
+ payCoinSelectionUid?: string;
totalPayCost: AmountString;
@@ -1803,7 +1832,7 @@ export interface DepositGroupRecord {
operationStatus: DepositOperationStatus;
- statusPerCoin: DepositElementStatus[];
+ statusPerCoin?: DepositElementStatus[];
infoPerExchange?: Record<string, DepositInfoPerExchange>;
@@ -1885,7 +1914,7 @@ export interface PeerPushDebitRecord {
totalCost: AmountString;
- coinSel: DbPeerPushPaymentCoinSelection;
+ coinSel?: DbPeerPushPaymentCoinSelection;
contractTermsHash: HashCodeString;
@@ -2197,6 +2226,11 @@ export interface CoinAvailabilityRecord {
* a final state.
*/
visibleCoinCount: number;
+
+ /**
+ * Number of coins that we expect to obtain via a pending refresh.
+ */
+ pendingRefreshOutputCount?: number;
}
export interface ContractTermsRecord {
@@ -2355,11 +2389,48 @@ export interface TransactionRecord {
currency: string;
}
+export enum DenomLossStatus {
+ /**
+ * Done indicates that the loss happened.
+ */
+ Done = 0x0500_0000,
+
+ /**
+ * Aborted in the sense that the loss was reversed.
+ */
+ Aborted = 0x0503_0001,
+}
+
+export interface DenomLossEventRecord {
+ denomLossEventId: string;
+ currency: string;
+ denomPubHashes: string[];
+ status: DenomLossStatus;
+ timestampCreated: DbPreciseTimestamp;
+ amount: string;
+ eventType: DenomLossEventType;
+ exchangeBaseUrl: string;
+}
+
/**
* Schema definition for the IndexedDB
* wallet database.
*/
export const WalletStoresV1 = {
+ denomLossEvents: describeStoreV2({
+ recordCodec: passthroughCodec<DenomLossEventRecord>(),
+ storeName: "denomLossEvents",
+ keyPath: "denomLossEventId",
+ versionAdded: 9,
+ indexes: {
+ byCurrency: describeIndex("byCurrency", "currency", {
+ versionAdded: 9,
+ }),
+ byStatus: describeIndex("byStatus", "status", {
+ versionAdded: 10,
+ }),
+ },
+ }),
transactions: describeStoreV2({
recordCodec: passthroughCodec<TransactionRecord>(),
storeName: "transactions",
@@ -3219,7 +3290,12 @@ export async function openStoredBackupsDatabase(
onStoredBackupsDbUpgradeNeeded,
);
- const handle = new DbAccessImpl(backupsDbHandle, StoredBackupStores);
+ const handle = new DbAccessImpl(
+ backupsDbHandle,
+ StoredBackupStores,
+ {},
+ CancellationToken.CONTINUE,
+ );
return handle;
}
@@ -3233,7 +3309,7 @@ export async function openStoredBackupsDatabase(
export async function openTalerDatabase(
idbFactory: IDBFactory,
onVersionChange: () => void,
-): Promise<DbAccess<typeof WalletStoresV1>> {
+): Promise<IDBDatabase> {
const metaDbHandle = await openDatabase(
idbFactory,
TALER_WALLET_META_DB_NAME,
@@ -3242,9 +3318,14 @@ export async function openTalerDatabase(
onMetaDbUpgradeNeeded,
);
- const metaDb = new DbAccessImpl(metaDbHandle, walletMetadataStore);
+ const metaDb = new DbAccessImpl(
+ metaDbHandle,
+ walletMetadataStore,
+ {},
+ CancellationToken.CONTINUE,
+ );
let currentMainVersion: string | undefined;
- await metaDb.runReadWriteTx(["metaConfig"], async (tx) => {
+ await metaDb.runReadWriteTx({ storeNames: ["metaConfig"] }, async (tx) => {
const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY);
if (!dbVersionRecord) {
currentMainVersion = TALER_WALLET_MAIN_DB_NAME;
@@ -3269,12 +3350,15 @@ export async function openTalerDatabase(
case "taler-wallet-main-v9":
// We consider this a pre-release
// development version, no migration is done.
- await metaDb.runReadWriteTx(["metaConfig"], async (tx) => {
- await tx.metaConfig.put({
- key: CURRENT_DB_CONFIG_KEY,
- value: TALER_WALLET_MAIN_DB_NAME,
- });
- });
+ await metaDb.runReadWriteTx(
+ { storeNames: ["metaConfig"] },
+ async (tx) => {
+ await tx.metaConfig.put({
+ key: CURRENT_DB_CONFIG_KEY,
+ value: TALER_WALLET_MAIN_DB_NAME,
+ });
+ },
+ );
break;
default:
throw Error(
@@ -3291,11 +3375,15 @@ export async function openTalerDatabase(
onTalerDbUpgradeNeeded,
);
- const handle = new DbAccessImpl(mainDbHandle, WalletStoresV1);
-
- await applyFixups(handle);
+ const mainDbAccess = new DbAccessImpl(
+ mainDbHandle,
+ WalletStoresV1,
+ {},
+ CancellationToken.CONTINUE,
+ );
+ await applyFixups(mainDbAccess);
- return handle;
+ return mainDbHandle;
}
export async function deleteTalerDatabase(
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
index 2d067a1dc..dfefe6ef5 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -105,7 +105,7 @@ export async function checkReserve(
}
}
-export interface TopupReserveWithDemobankArgs {
+export interface TopupReserveWithBankArgs {
http: HttpRequestLibrary;
reservePub: string;
corebankApiBaseUrl: string;
@@ -113,8 +113,8 @@ export interface TopupReserveWithDemobankArgs {
amount: AmountString;
}
-export async function topupReserveWithDemobank(
- args: TopupReserveWithDemobankArgs,
+export async function topupReserveWithBank(
+ args: TopupReserveWithBankArgs,
) {
const { http, corebankApiBaseUrl, amount, exchangeInfo, reservePub } = args;
const bankClient = new TalerCorebankApiClient(corebankApiBaseUrl);
diff --git a/packages/taler-wallet-core/src/denomSelection.ts b/packages/taler-wallet-core/src/denomSelection.ts
index dd5ec60d8..ecc1fa881 100644
--- a/packages/taler-wallet-core/src/denomSelection.ts
+++ b/packages/taler-wallet-core/src/denomSelection.ts
@@ -24,13 +24,14 @@
* Imports.
*/
import {
+ AbsoluteTime,
AmountJson,
Amounts,
DenomSelectionState,
ForcedDenomSel,
Logger,
} from "@gnu-taler/taler-util";
-import { DenominationRecord } from "./db.js";
+import { DenominationRecord, timestampAbsoluteFromDb } from "./db.js";
import { isWithdrawableDenom } from "./denominations.js";
const logger = new Logger("denomSelection.ts");
@@ -54,6 +55,8 @@ export function selectWithdrawalDenominations(
let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
+ let earliestDepositExpiration: AbsoluteTime | undefined;
+ let hasDenomWithAgeRestriction = false;
denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate));
denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
@@ -82,6 +85,17 @@ export function selectWithdrawalDenominations(
count,
denomPubHash: d.denomPubHash,
});
+ hasDenomWithAgeRestriction =
+ hasDenomWithAgeRestriction || d.denomPub.age_mask > 0;
+ const expireDeposit = timestampAbsoluteFromDb(d.stampExpireDeposit);
+ if (!earliestDepositExpiration) {
+ earliestDepositExpiration = expireDeposit;
+ } else {
+ earliestDepositExpiration = AbsoluteTime.min(
+ expireDeposit,
+ earliestDepositExpiration,
+ );
+ }
}
if (logger.shouldLogTrace()) {
@@ -103,10 +117,16 @@ export function selectWithdrawalDenominations(
logger.trace("(end of denom selection)");
}
+ earliestDepositExpiration ??= AbsoluteTime.never();
+
return {
selectedDenoms,
totalCoinValue: Amounts.stringify(totalCoinValue),
totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
+ earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
+ earliestDepositExpiration,
+ ),
+ hasDenomWithAgeRestriction,
};
}
@@ -123,6 +143,8 @@ export function selectForcedWithdrawalDenominations(
let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
+ let earliestDepositExpiration: AbsoluteTime | undefined;
+ let hasDenomWithAgeRestriction = false;
denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate));
denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
@@ -150,11 +172,28 @@ export function selectForcedWithdrawalDenominations(
count,
denomPubHash: denom.denomPubHash,
});
+ hasDenomWithAgeRestriction =
+ hasDenomWithAgeRestriction || denom.denomPub.age_mask > 0;
+ const expireDeposit = timestampAbsoluteFromDb(denom.stampExpireDeposit);
+ if (!earliestDepositExpiration) {
+ earliestDepositExpiration = expireDeposit;
+ } else {
+ earliestDepositExpiration = AbsoluteTime.min(
+ expireDeposit,
+ earliestDepositExpiration,
+ );
+ }
}
+ earliestDepositExpiration ??= AbsoluteTime.never();
+
return {
selectedDenoms,
totalCoinValue: Amounts.stringify(totalCoinValue),
totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
+ earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
+ earliestDepositExpiration,
+ ),
+ hasDenomWithAgeRestriction,
};
}
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
index 6c04b20de..c4cd98d73 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -39,10 +39,10 @@ import {
Logger,
MerchantContractTerms,
NotificationType,
- PayCoinSelection,
PrepareDepositRequest,
PrepareDepositResponse,
RefreshReason,
+ SelectedProspectiveCoin,
TalerError,
TalerErrorCode,
TalerPreciseTimestamp,
@@ -110,7 +110,6 @@ import {
parseTransactionIdentifier,
} from "./transactions.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
-import { getCandidateWithdrawalDenomsTx } from "./withdraw.js";
/**
* Logger.
@@ -140,22 +139,25 @@ export class DepositTransactionContext implements TransactionContext {
const ws = this.wex;
// FIXME: We should check first if we are in a final state
// where deletion is allowed.
- await ws.db.runReadWriteTx(["depositGroups", "tombstones"], async (tx) => {
- const tipRecord = await tx.depositGroups.get(depositGroupId);
- if (tipRecord) {
- await tx.depositGroups.delete(depositGroupId);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
- });
- }
- });
+ await ws.db.runReadWriteTx(
+ { storeNames: ["depositGroups", "tombstones"] },
+ async (tx) => {
+ const tipRecord = await tx.depositGroups.get(depositGroupId);
+ if (tipRecord) {
+ await tx.depositGroups.delete(depositGroupId);
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
+ });
+ }
+ },
+ );
return;
}
async suspendTransaction(): Promise<void> {
const { wex, depositGroupId, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["depositGroups"],
+ { storeNames: ["depositGroups"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -198,7 +200,7 @@ export class DepositTransactionContext implements TransactionContext {
async abortTransaction(): Promise<void> {
const { wex, depositGroupId, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["depositGroups"],
+ { storeNames: ["depositGroups"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -236,7 +238,7 @@ export class DepositTransactionContext implements TransactionContext {
async resumeTransaction(): Promise<void> {
const { wex, depositGroupId, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["depositGroups"],
+ { storeNames: ["depositGroups"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -279,7 +281,7 @@ export class DepositTransactionContext implements TransactionContext {
async failTransaction(): Promise<void> {
const { wex, depositGroupId, transactionId, taskId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["depositGroups"],
+ { storeNames: ["depositGroups"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -413,25 +415,37 @@ async function refundDepositGroup(
wex: WalletExecutionContext,
depositGroup: DepositGroupRecord,
): Promise<TaskRunResult> {
- const newTxPerCoin = [...depositGroup.statusPerCoin];
+ const statusPerCoin = depositGroup.statusPerCoin;
+ const payCoinSelection = depositGroup.payCoinSelection;
+ if (!statusPerCoin) {
+ throw Error(
+ "unable to refund deposit group without coin selection (status missing)",
+ );
+ }
+ if (!payCoinSelection) {
+ throw Error(
+ "unable to refund deposit group without coin selection (selection missing)",
+ );
+ }
+ const newTxPerCoin = [...statusPerCoin];
logger.info(`status per coin: ${j2s(depositGroup.statusPerCoin)}`);
- for (let i = 0; i < depositGroup.statusPerCoin.length; i++) {
- const st = depositGroup.statusPerCoin[i];
+ for (let i = 0; i < statusPerCoin.length; i++) {
+ const st = statusPerCoin[i];
switch (st) {
case DepositElementStatus.RefundFailed:
case DepositElementStatus.RefundSuccess:
break;
default: {
- const coinPub = depositGroup.payCoinSelection.coinPubs[i];
+ const coinPub = payCoinSelection.coinPubs[i];
const coinExchange = await wex.db.runReadOnlyTx(
- ["coins"],
+ { storeNames: ["coins"] },
async (tx) => {
const coinRecord = await tx.coins.get(coinPub);
checkDbInvariant(!!coinRecord);
return coinRecord.exchangeBaseUrl;
},
);
- const refundAmount = depositGroup.payCoinSelection.coinContributions[i];
+ const refundAmount = payCoinSelection.coinContributions[i];
// We use a constant refund transaction ID, since there can
// only be one refund.
const rtid = 1;
@@ -486,13 +500,16 @@ async function refundDepositGroup(
const currency = Amounts.currencyOf(depositGroup.totalPayCost);
const res = await wex.db.runReadWriteTx(
- [
- "depositGroups",
- "refreshGroups",
- "coins",
- "denominations",
- "coinAvailability",
- ],
+ {
+ storeNames: [
+ "depositGroups",
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
async (tx) => {
const newDg = await tx.depositGroups.get(depositGroup.depositGroupId);
if (!newDg) {
@@ -502,8 +519,8 @@ async function refundDepositGroup(
const refreshCoins: CoinRefreshRequest[] = [];
for (let i = 0; i < newTxPerCoin.length; i++) {
refreshCoins.push({
- amount: depositGroup.payCoinSelection.coinContributions[i],
- coinPub: depositGroup.payCoinSelection.coinPubs[i],
+ amount: payCoinSelection.coinContributions[i],
+ coinPub: payCoinSelection.coinPubs[i],
});
}
let refreshRes: CreateRefreshGroupResult | undefined = undefined;
@@ -559,7 +576,7 @@ async function waitForRefreshOnDepositGroup(
depositGroupId: depositGroup.depositGroupId,
});
const transitionInfo = await wex.db.runReadWriteTx(
- ["depositGroups", "refreshGroups"],
+ { storeNames: ["depositGroups", "refreshGroups"] },
async (tx) => {
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
let newOpState: DepositOperationStatus | undefined;
@@ -648,7 +665,7 @@ async function processDepositGroupPendingKyc(
kycStatusRes.status === HttpStatusCode.NoContent
) {
const transitionInfo = await wex.db.runReadWriteTx(
- ["depositGroups"],
+ { storeNames: ["depositGroups"] },
async (tx) => {
const newDg = await tx.depositGroups.get(depositGroupId);
if (!newDg) {
@@ -707,7 +724,7 @@ async function transitionToKycRequired(
const kycStatus = await kycStatusReq.json();
logger.info(`kyc status: ${j2s(kycStatus)}`);
const transitionInfo = await wex.db.runReadWriteTx(
- ["depositGroups"],
+ { storeNames: ["depositGroups"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -739,15 +756,27 @@ async function processDepositGroupPendingTrack(
wex: WalletExecutionContext,
depositGroup: DepositGroupRecord,
): Promise<TaskRunResult> {
+ const statusPerCoin = depositGroup.statusPerCoin;
+ const payCoinSelection = depositGroup.payCoinSelection;
+ if (!statusPerCoin) {
+ throw Error(
+ "unable to refund deposit group without coin selection (status missing)",
+ );
+ }
+ if (!payCoinSelection) {
+ throw Error(
+ "unable to refund deposit group without coin selection (selection missing)",
+ );
+ }
const { depositGroupId } = depositGroup;
- for (let i = 0; i < depositGroup.statusPerCoin.length; i++) {
- const coinPub = depositGroup.payCoinSelection.coinPubs[i];
+ for (let i = 0; i < statusPerCoin.length; i++) {
+ const coinPub = payCoinSelection.coinPubs[i];
// FIXME: Make the URL part of the coin selection?
const exchangeBaseUrl = await wex.db.runReadWriteTx(
- ["coins"],
+ { storeNames: ["coins"] },
async (tx) => {
const coinRecord = await tx.coins.get(coinPub);
- checkDbInvariant(!!coinRecord);
+ checkDbInvariant(!!coinRecord, `coin ${coinPub} not found in DB`);
return coinRecord.exchangeBaseUrl;
},
);
@@ -755,12 +784,12 @@ async function processDepositGroupPendingTrack(
let updatedTxStatus: DepositElementStatus | undefined = undefined;
let newWiredCoin:
| {
- id: string;
- value: DepositTrackingInfo;
- }
+ id: string;
+ value: DepositTrackingInfo;
+ }
| undefined;
- if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) {
+ if (statusPerCoin[i] !== DepositElementStatus.Wired) {
const track = await trackDeposit(
wex,
depositGroup,
@@ -820,46 +849,55 @@ async function processDepositGroupPendingTrack(
}
if (updatedTxStatus !== undefined) {
- await wex.db.runReadWriteTx(["depositGroups"], async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- return;
- }
- if (updatedTxStatus !== undefined) {
- dg.statusPerCoin[i] = updatedTxStatus;
- }
- if (newWiredCoin) {
- /**
- * FIXME: if there is a new wire information from the exchange
- * it should add up to the previous tracking states.
- *
- * This may loose information by overriding prev state.
- *
- * And: add checks to integration tests
- */
- if (!dg.trackingState) {
- dg.trackingState = {};
+ await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return;
}
-
- dg.trackingState[newWiredCoin.id] = newWiredCoin.value;
- }
- await tx.depositGroups.put(dg);
- });
+ if (!dg.statusPerCoin) {
+ return;
+ }
+ if (updatedTxStatus !== undefined) {
+ dg.statusPerCoin[i] = updatedTxStatus;
+ }
+ if (newWiredCoin) {
+ /**
+ * FIXME: if there is a new wire information from the exchange
+ * it should add up to the previous tracking states.
+ *
+ * This may loose information by overriding prev state.
+ *
+ * And: add checks to integration tests
+ */
+ if (!dg.trackingState) {
+ dg.trackingState = {};
+ }
+
+ dg.trackingState[newWiredCoin.id] = newWiredCoin.value;
+ }
+ await tx.depositGroups.put(dg);
+ },
+ );
}
}
let allWired = true;
const transitionInfo = await wex.db.runReadWriteTx(
- ["depositGroups"],
+ { storeNames: ["depositGroups"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
return undefined;
}
+ if (!dg.statusPerCoin) {
+ return undefined;
+ }
const oldTxState = computeDepositTransactionStatus(dg);
- for (let i = 0; i < depositGroup.statusPerCoin.length; i++) {
- if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) {
+ for (let i = 0; i < dg.statusPerCoin.length; i++) {
+ if (dg.statusPerCoin[i] !== DepositElementStatus.Wired) {
allWired = false;
break;
}
@@ -899,7 +937,7 @@ async function processDepositGroupPendingDeposit(
logger.info("processing deposit group in pending(deposit)");
const depositGroupId = depositGroup.depositGroupId;
const contractTermsRec = await wex.db.runReadOnlyTx(
- ["contractTerms"],
+ { storeNames: ["contractTerms"] },
async (tx) => {
return tx.contractTerms.get(depositGroup.contractTermsHash);
},
@@ -923,6 +961,88 @@ async function processDepositGroupPendingDeposit(
// Check for cancellation before expensive operations.
cancellationToken?.throwIfCancelled();
+ if (!depositGroup.payCoinSelection) {
+ logger.info("missing coin selection for deposit group, selecting now");
+ // FIXME: Consider doing the coin selection inside the txn
+ const payCoinSel = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins: [],
+ });
+
+ switch (payCoinSel.type) {
+ case "success":
+ logger.info("coin selection success");
+ break;
+ case "failure":
+ logger.info("coin selection failure");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ logger.info("coin selection prospective");
+ throw Error("insufficient balance (waiting on pending refresh)");
+ default:
+ assertUnreachable(payCoinSel);
+ }
+
+ const transitionDone = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "depositGroups",
+ "coins",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ ],
+ },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return false;
+ }
+ if (dg.statusPerCoin) {
+ return false;
+ }
+ dg.payCoinSelection = {
+ coinContributions: payCoinSel.coinSel.coins.map(
+ (x) => x.contribution,
+ ),
+ coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
+ };
+ dg.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
+ dg.statusPerCoin = payCoinSel.coinSel.coins.map(
+ () => DepositElementStatus.DepositPending,
+ );
+ await tx.depositGroups.put(dg);
+ await spendCoins(wex, tx, {
+ allocationId: transactionId,
+ coinPubs: dg.payCoinSelection.coinPubs,
+ contributions: dg.payCoinSelection.coinContributions.map((x) =>
+ Amounts.parseOrThrow(x),
+ ),
+ refreshReason: RefreshReason.PayDeposit,
+ });
+ return true;
+ },
+ );
+
+ if (transitionDone) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
+ }
+
// FIXME: Cache these!
const depositPermissions = await generateDepositPermissions(
wex,
@@ -984,24 +1104,30 @@ async function processDepositGroupPendingDeposit(
codecForBatchDepositSuccess(),
);
- await wex.db.runReadWriteTx(["depositGroups"], async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- return;
- }
- for (const batchIndex of batchIndexes) {
- const coinStatus = dg.statusPerCoin[batchIndex];
- switch (coinStatus) {
- case DepositElementStatus.DepositPending:
- dg.statusPerCoin[batchIndex] = DepositElementStatus.Tracking;
- await tx.depositGroups.put(dg);
+ await wex.db.runReadWriteTx(
+ { storeNames: ["depositGroups"] },
+ async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return;
}
- }
- });
+ if (!dg.statusPerCoin) {
+ return;
+ }
+ for (const batchIndex of batchIndexes) {
+ const coinStatus = dg.statusPerCoin[batchIndex];
+ switch (coinStatus) {
+ case DepositElementStatus.DepositPending:
+ dg.statusPerCoin[batchIndex] = DepositElementStatus.Tracking;
+ await tx.depositGroups.put(dg);
+ }
+ }
+ },
+ );
}
const transitionInfo = await wex.db.runReadWriteTx(
- ["depositGroups"],
+ { storeNames: ["depositGroups"] },
async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
@@ -1027,7 +1153,7 @@ export async function processDepositGroup(
depositGroupId: string,
): Promise<TaskRunResult> {
const depositGroup = await wex.db.runReadOnlyTx(
- ["depositGroups"],
+ { storeNames: ["depositGroups"] },
async (tx) => {
return tx.depositGroups.get(depositGroupId);
},
@@ -1061,7 +1187,7 @@ async function getExchangeWireFee(
time: TalerProtocolTimestamp,
): Promise<WireFee> {
const exchangeDetails = await wex.db.runReadOnlyTx(
- ["exchangeDetails", "exchanges"],
+ { storeNames: ["exchangeDetails", "exchanges"] },
async (tx) => {
const ex = await tx.exchanges.get(baseUrl);
if (!ex || !ex.detailsPointer) return undefined;
@@ -1154,11 +1280,8 @@ async function trackDeposit(
/**
* Check if creating a deposit group is possible and calculate
* the associated fees.
- *
- * FIXME: This should be renamed to checkDepositGroup,
- * as it doesn't prepare anything
*/
-export async function prepareDepositGroup(
+export async function checkDepositGroup(
wex: WalletExecutionContext,
req: PrepareDepositRequest,
): Promise<PrepareDepositResponse> {
@@ -1167,22 +1290,26 @@ export async function prepareDepositGroup(
throw Error("invalid payto URI");
}
const amount = Amounts.parseOrThrow(req.amount);
+ const currency = Amounts.currencyOf(amount);
const exchangeInfos: ExchangeHandle[] = [];
- await wex.db.runReadOnlyTx(["exchangeDetails", "exchanges"], async (tx) => {
- const allExchanges = await tx.exchanges.iter().toArray();
- for (const e of allExchanges) {
- const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
- if (!details || amount.currency !== details.currency) {
- continue;
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["exchangeDetails", "exchanges"] },
+ async (tx) => {
+ const allExchanges = await tx.exchanges.iter().toArray();
+ for (const e of allExchanges) {
+ const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
+ if (!details || amount.currency !== details.currency) {
+ continue;
+ }
+ exchangeInfos.push({
+ master_pub: details.masterPublicKey,
+ url: e.baseUrl,
+ });
}
- exchangeInfos.push({
- master_pub: details.masterPublicKey,
- url: e.baseUrl,
- });
- }
- });
+ },
+ );
const now = AbsoluteTime.now();
const nowRounded = AbsoluteTime.toProtocolTimestamp(now);
@@ -1226,32 +1353,42 @@ export async function prepareDepositGroup(
restrictWireMethod: contractData.wireMethod,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- wireFeeAmortization: 1, // FIXME #8653
prevPayCoins: [],
});
- if (payCoinSel.type !== "success") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
- },
- );
+ let selCoins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (payCoinSel.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ selCoins = payCoinSel.result.prospectiveCoins;
+ break;
+ case "success":
+ selCoins = payCoinSel.coinSel.coins;
+ break;
+ default:
+ assertUnreachable(payCoinSel);
}
- const totalDepositCost = await getTotalPaymentCost(wex, payCoinSel.coinSel);
+ const totalDepositCost = await getTotalPaymentCost(wex, currency, selCoins);
const effectiveDepositAmount = await getCounterpartyEffectiveDepositAmount(
wex,
p.targetType,
- payCoinSel.coinSel,
+ selCoins,
);
const fees = await getTotalFeesForDepositAmount(
wex,
p.targetType,
amount,
- payCoinSel.coinSel,
+ selCoins,
);
return {
@@ -1279,22 +1416,26 @@ export async function createDepositGroup(
}
const amount = Amounts.parseOrThrow(req.amount);
+ const currency = amount.currency;
const exchangeInfos: { url: string; master_pub: string }[] = [];
- await wex.db.runReadOnlyTx(["exchanges", "exchangeDetails"], async (tx) => {
- const allExchanges = await tx.exchanges.iter().toArray();
- for (const e of allExchanges) {
- const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
- if (!details || amount.currency !== details.currency) {
- continue;
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges", "exchangeDetails"] },
+ async (tx) => {
+ const allExchanges = await tx.exchanges.iter().toArray();
+ for (const e of allExchanges) {
+ const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
+ if (!details || amount.currency !== details.currency) {
+ continue;
+ }
+ exchangeInfos.push({
+ master_pub: details.masterPublicKey,
+ url: e.baseUrl,
+ });
}
- exchangeInfos.push({
- master_pub: details.masterPublicKey,
- url: e.baseUrl,
- });
- }
- });
+ },
+ );
const now = AbsoluteTime.now();
const wireDeadline = AbsoluteTime.toProtocolTimestamp(
@@ -1345,20 +1486,30 @@ export async function createDepositGroup(
restrictWireMethod: contractData.wireMethod,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- wireFeeAmortization: 1, // FIXME #8653
prevPayCoins: [],
});
- if (payCoinSel.type !== "success") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
- },
- );
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (payCoinSel.type) {
+ case "success":
+ coins = payCoinSel.coinSel.coins;
+ break;
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = payCoinSel.result.prospectiveCoins;
+ break;
+ default:
+ assertUnreachable(payCoinSel);
}
- const totalDepositCost = await getTotalPaymentCost(wex, payCoinSel.coinSel);
+ const totalDepositCost = await getTotalPaymentCost(wex, currency, coins);
let depositGroupId: string;
if (req.transactionId) {
@@ -1373,34 +1524,23 @@ export async function createDepositGroup(
const infoPerExchange: Record<string, DepositInfoPerExchange> = {};
- await wex.db.runReadOnlyTx(["coins"], async (tx) => {
- for (let i = 0; i < payCoinSel.coinSel.coins.length; i++) {
- const coin = await tx.coins.get(payCoinSel.coinSel.coins[i].coinPub);
- if (!coin) {
- logger.error("coin not found anymore");
- continue;
- }
- let depPerExchange = infoPerExchange[coin.exchangeBaseUrl];
- if (!depPerExchange) {
- infoPerExchange[coin.exchangeBaseUrl] = depPerExchange = {
- amountEffective: Amounts.stringify(
- Amounts.zeroOfAmount(totalDepositCost),
- ),
- };
- }
- const contrib = payCoinSel.coinSel.coins[i].contribution;
- depPerExchange.amountEffective = Amounts.stringify(
- Amounts.add(depPerExchange.amountEffective, contrib).amount,
- );
+ for (let i = 0; i < coins.length; i++) {
+ let depPerExchange = infoPerExchange[coins[i].exchangeBaseUrl];
+ if (!depPerExchange) {
+ infoPerExchange[coins[i].exchangeBaseUrl] = depPerExchange = {
+ amountEffective: Amounts.stringify(
+ Amounts.zeroOfAmount(totalDepositCost),
+ ),
+ };
}
- });
+ const contrib = coins[i].contribution;
+ depPerExchange.amountEffective = Amounts.stringify(
+ Amounts.add(depPerExchange.amountEffective, contrib).amount,
+ );
+ }
const counterpartyEffectiveDepositAmount =
- await getCounterpartyEffectiveDepositAmount(
- wex,
- p.targetType,
- payCoinSel.coinSel,
- );
+ await getCounterpartyEffectiveDepositAmount(wex, p.targetType, coins);
const depositGroup: DepositGroupRecord = {
contractTermsHash,
@@ -1413,14 +1553,9 @@ export async function createDepositGroup(
AbsoluteTime.toPreciseTimestamp(now),
),
timestampFinished: undefined,
- statusPerCoin: payCoinSel.coinSel.coins.map(
- () => DepositElementStatus.DepositPending,
- ),
- payCoinSelection: {
- coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution),
- coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
- },
- payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
+ statusPerCoin: undefined,
+ payCoinSelection: undefined,
+ payCoinSelectionUid: undefined,
merchantPriv: merchantPair.priv,
merchantPub: merchantPair.pub,
totalPayCost: Amounts.stringify(totalDepositCost),
@@ -1438,28 +1573,44 @@ export async function createDepositGroup(
infoPerExchange,
};
+ if (payCoinSel.type === "success") {
+ depositGroup.payCoinSelection = {
+ coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution),
+ coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
+ };
+ depositGroup.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
+ depositGroup.statusPerCoin = payCoinSel.coinSel.coins.map(
+ () => DepositElementStatus.DepositPending,
+ );
+ }
+
const ctx = new DepositTransactionContext(wex, depositGroupId);
const transactionId = ctx.transactionId;
const newTxState = await wex.db.runReadWriteTx(
- [
- "depositGroups",
- "coins",
- "recoupGroups",
- "denominations",
- "refreshGroups",
- "coinAvailability",
- "contractTerms",
- ],
+ {
+ storeNames: [
+ "depositGroups",
+ "coins",
+ "recoupGroups",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "coinAvailability",
+ "contractTerms",
+ ],
+ },
async (tx) => {
- await spendCoins(wex, tx, {
- allocationId: transactionId,
- coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
- contributions: payCoinSel.coinSel.coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- ),
- refreshReason: RefreshReason.PayDeposit,
- });
+ if (depositGroup.payCoinSelection) {
+ await spendCoins(wex, tx, {
+ allocationId: transactionId,
+ coinPubs: depositGroup.payCoinSelection.coinPubs,
+ contributions: depositGroup.payCoinSelection.coinContributions.map(
+ (x) => Amounts.parseOrThrow(x),
+ ),
+ refreshReason: RefreshReason.PayDeposit,
+ });
+ }
await tx.depositGroups.put(depositGroup);
await tx.contractTerms.put({
contractTermsRaw: contractTerms,
@@ -1498,32 +1649,28 @@ export async function createDepositGroup(
export async function getCounterpartyEffectiveDepositAmount(
wex: WalletExecutionContext,
wireType: string,
- pcs: PayCoinSelection,
+ pcs: SelectedProspectiveCoin[],
): Promise<AmountJson> {
const amt: AmountJson[] = [];
const fees: AmountJson[] = [];
const exchangeSet: Set<string> = new Set();
await wex.db.runReadOnlyTx(
- ["coins", "denominations", "exchangeDetails", "exchanges"],
+ { storeNames: ["coins", "denominations", "exchangeDetails", "exchanges"] },
async (tx) => {
- for (let i = 0; i < pcs.coins.length; i++) {
- const coin = await tx.coins.get(pcs.coins[i].coinPub);
- if (!coin) {
- throw Error("can't calculate deposit amount, coin not found");
- }
+ for (let i = 0; i < pcs.length; i++) {
const denom = await getDenomInfo(
wex,
tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
);
if (!denom) {
throw Error("can't find denomination to calculate deposit amount");
}
- amt.push(Amounts.parseOrThrow(pcs.coins[i].contribution));
+ amt.push(Amounts.parseOrThrow(pcs[i].contribution));
fees.push(Amounts.parseOrThrow(denom.feeDeposit));
- exchangeSet.add(coin.exchangeBaseUrl);
+ exchangeSet.add(pcs[i].exchangeBaseUrl);
}
for (const exchangeUrl of exchangeSet.values()) {
@@ -1562,49 +1709,34 @@ async function getTotalFeesForDepositAmount(
wex: WalletExecutionContext,
wireType: string,
total: AmountJson,
- pcs: PayCoinSelection,
+ pcs: SelectedProspectiveCoin[],
): Promise<DepositGroupFees> {
const wireFee: AmountJson[] = [];
const coinFee: AmountJson[] = [];
const refreshFee: AmountJson[] = [];
const exchangeSet: Set<string> = new Set();
- const currency = Amounts.currencyOf(total);
await wex.db.runReadOnlyTx(
- ["coins", "denominations", "exchanges", "exchangeDetails"],
+ { storeNames: ["coins", "denominations", "exchanges", "exchangeDetails"] },
async (tx) => {
- for (let i = 0; i < pcs.coins.length; i++) {
- const coin = await tx.coins.get(pcs.coins[i].coinPub);
- if (!coin) {
- throw Error("can't calculate deposit amount, coin not found");
- }
+ for (let i = 0; i < pcs.length; i++) {
const denom = await getDenomInfo(
wex,
tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
);
if (!denom) {
throw Error("can't find denomination to calculate deposit amount");
}
coinFee.push(Amounts.parseOrThrow(denom.feeDeposit));
- exchangeSet.add(coin.exchangeBaseUrl);
-
- const allDenoms = await getCandidateWithdrawalDenomsTx(
+ exchangeSet.add(pcs[i].exchangeBaseUrl);
+ const amountLeft = Amounts.sub(denom.value, pcs[i].contribution).amount;
+ const refreshCost = await getTotalRefreshCost(
wex,
tx,
- coin.exchangeBaseUrl,
- currency,
- );
- const amountLeft = Amounts.sub(
- denom.value,
- pcs.coins[i].contribution,
- ).amount;
- const refreshCost = getTotalRefreshCost(
- allDenoms,
denom,
amountLeft,
- wex.ws.config.testing.denomselAllowLate,
);
refreshFee.push(refreshCost);
}
diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts
index c94571ff8..5cb9400be 100644
--- a/packages/taler-wallet-core/src/dev-experiments.ts
+++ b/packages/taler-wallet-core/src/dev-experiments.ts
@@ -26,6 +26,7 @@
*/
import {
+ DenomLossEventType,
Logger,
RefreshReason,
TalerPreciseTimestamp,
@@ -38,7 +39,10 @@ import {
HttpRequestOptions,
HttpResponse,
} from "@gnu-taler/taler-util/http";
+import { PendingTaskType, constructTaskIdentifier } from "./common.js";
import {
+ DenomLossEventRecord,
+ DenomLossStatus,
RefreshGroupRecord,
RefreshOperationStatus,
timestampPreciseToDb,
@@ -61,31 +65,71 @@ export async function applyDevExperiment(
return;
}
if (!wex.ws.config.testing.devModeActive) {
- throw Error(
- "can't handle devmode URI (other than enable-devmode) unless devmode is active",
- );
+ throw Error("can't handle devmode URI unless devmode is active");
}
- if (parsedUri.devExperimentId == "insert-pending-refresh") {
- await wex.db.runReadWriteTx(["refreshGroups"], async (tx) => {
+ switch (parsedUri.devExperimentId) {
+ case "start-block-refresh": {
+ wex.ws.devExperimentState.blockRefreshes = true;
+ return;
+ }
+ case "stop-block-refresh": {
+ wex.ws.devExperimentState.blockRefreshes = false;
+ return;
+ }
+ case "insert-pending-refresh": {
const refreshGroupId = encodeCrock(getRandomBytes(32));
- const newRg: RefreshGroupRecord = {
- currency: "TESTKUDOS",
- expectedOutputPerCoin: [],
- inputPerCoin: [],
- oldCoinPubs: [],
- operationStatus: RefreshOperationStatus.Pending,
- reason: RefreshReason.Manual,
- refreshGroupId,
- statusPerCoin: [],
- timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
- timestampFinished: undefined,
- originatingTransactionId: undefined,
- infoPerExchange: {},
- };
- await tx.refreshGroups.put(newRg);
- });
- return;
+ await wex.db.runReadWriteTx(
+ { storeNames: ["refreshGroups"] },
+ async (tx) => {
+ const newRg: RefreshGroupRecord = {
+ currency: "TESTKUDOS",
+ expectedOutputPerCoin: [],
+ inputPerCoin: [],
+ oldCoinPubs: [],
+ operationStatus: RefreshOperationStatus.Pending,
+ reason: RefreshReason.Manual,
+ refreshGroupId,
+ statusPerCoin: [],
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ timestampFinished: undefined,
+ originatingTransactionId: undefined,
+ infoPerExchange: {},
+ };
+ await tx.refreshGroups.put(newRg);
+ },
+ );
+ wex.taskScheduler.startShepherdTask(
+ constructTaskIdentifier({
+ tag: PendingTaskType.Refresh,
+ refreshGroupId,
+ }),
+ );
+ return;
+ }
+ case "insert-denom-loss": {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["denomLossEvents"] },
+ async (tx) => {
+ const eventId = encodeCrock(getRandomBytes(32));
+ const newRg: DenomLossEventRecord = {
+ amount: "TESTKUDOS:42",
+ currency: "TESTKUDOS",
+ exchangeBaseUrl: "https://exchange.test.taler.net/",
+ denomLossEventId: eventId,
+ denomPubHashes: [
+ encodeCrock(getRandomBytes(64)),
+ encodeCrock(getRandomBytes(64)),
+ ],
+ eventType: DenomLossEventType.DenomExpired,
+ status: DenomLossStatus.Done,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ };
+ await tx.denomLossEvents.put(newRg);
+ },
+ );
+ return;
+ }
}
throw Error(`dev-experiment id not understood ${parsedUri.devExperimentId}`);
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
index 8b4bca2aa..6262ae4d3 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -26,6 +26,7 @@
import {
AbsoluteTime,
AgeRestriction,
+ Amount,
Amounts,
AsyncFlag,
CancellationToken,
@@ -33,6 +34,7 @@ import {
CoinStatus,
DeleteExchangeRequest,
DenomKeyType,
+ DenomLossEventType,
DenomOperationMap,
DenominationInfo,
DenominationPubKey,
@@ -65,6 +67,10 @@ import {
TalerPreciseTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionState,
+ TransactionType,
URL,
WalletNotification,
WireFee,
@@ -72,11 +78,11 @@ import {
WireFeesJson,
WireInfo,
assertUnreachable,
- canonicalizeBaseUrl,
checkDbInvariant,
codecForExchangeKeysJson,
durationMul,
encodeCrock,
+ getRandomBytes,
hashDenomPub,
j2s,
makeErrorDetail,
@@ -90,9 +96,12 @@ import {
} from "@gnu-taler/taler-util/http";
import {
PendingTaskType,
+ TaskIdStr,
TaskIdentifiers,
TaskRunResult,
TaskRunResultType,
+ TransactionContext,
+ computeDbBackoff,
constructTaskIdentifier,
getAutoRefreshExecuteThreshold,
getExchangeEntryStatusFromRecord,
@@ -101,6 +110,8 @@ import {
getExchangeUpdateStatusFromRecord,
} from "./common.js";
import {
+ DenomLossEventRecord,
+ DenomLossStatus,
DenominationRecord,
DenominationVerificationStatus,
ExchangeDetailsRecord,
@@ -126,6 +137,10 @@ import {
import { DbReadOnlyTransaction } from "./query.js";
import { createRecoupGroup } from "./recoup.js";
import { createRefreshGroup } from "./refresh.js";
+import {
+ constructTransactionIdentifier,
+ notifyTransition,
+} from "./transactions.js";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
import { InternalWalletState, WalletExecutionContext } from "./wallet.js";
@@ -311,7 +326,7 @@ async function makeExchangeListItem(
masterPub: exchangeDetails?.masterPublicKey,
noFees: r.noFees ?? false,
peerPaymentsDisabled: r.peerPaymentsDisabled ?? false,
- currency: exchangeDetails?.currency ?? r.presetCurrencyHint,
+ currency: exchangeDetails?.currency ?? r.presetCurrencyHint ?? "UNKNOWN",
exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r),
exchangeEntryStatus: getExchangeEntryStatusFromRecord(r),
tosStatus: getExchangeTosStatusFromRecord(r),
@@ -321,7 +336,11 @@ async function makeExchangeListItem(
paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [],
lastUpdateTimestamp: timestampOptionalPreciseFromDb(r.lastUpdate),
lastUpdateErrorInfo,
- scopeInfo,
+ scopeInfo: scopeInfo ?? {
+ type: ScopeType.Exchange,
+ currency: "UNKNOWN",
+ url: r.baseUrl,
+ },
};
}
@@ -357,13 +376,15 @@ export async function lookupExchangeByUri(
req: GetExchangeEntryByUrlRequest,
): Promise<ExchangeListItem> {
return await wex.db.runReadOnlyTx(
- [
- "exchanges",
- "exchangeDetails",
- "operationRetries",
- "globalCurrencyAuditors",
- "globalCurrencyExchanges",
- ],
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "operationRetries",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
async (tx) => {
const exchangeRec = await tx.exchanges.get(req.exchangeBaseUrl);
if (!exchangeRec) {
@@ -394,7 +415,7 @@ export async function acceptExchangeTermsOfService(
exchangeBaseUrl: string,
): Promise<void> {
const notif = await wex.db.runReadWriteTx(
- ["exchangeDetails", "exchanges"],
+ { storeNames: ["exchangeDetails", "exchanges"] },
async (tx) => {
const exch = await tx.exchanges.get(exchangeBaseUrl);
if (exch && exch.tosCurrentEtag) {
@@ -405,6 +426,7 @@ export async function acceptExchangeTermsOfService(
);
await tx.exchanges.put(exch);
const newExchangeState = getExchangeState(exch);
+ wex.ws.exchangeCache.clear();
return {
type: NotificationType.ExchangeStateTransition,
exchangeBaseUrl,
@@ -428,7 +450,7 @@ export async function forgetExchangeTermsOfService(
exchangeBaseUrl: string,
): Promise<void> {
const notif = await wex.db.runReadWriteTx(
- ["exchangeDetails", "exchanges"],
+ { storeNames: ["exchangeDetails", "exchanges"] },
async (tx) => {
const exch = await tx.exchanges.get(exchangeBaseUrl);
if (exch) {
@@ -437,6 +459,7 @@ export async function forgetExchangeTermsOfService(
exch.tosAcceptedTimestamp = undefined;
await tx.exchanges.put(exch);
const newExchangeState = getExchangeState(exch);
+ wex.ws.exchangeCache.clear();
return {
type: NotificationType.ExchangeStateTransition,
exchangeBaseUrl,
@@ -890,17 +913,16 @@ async function startUpdateExchangeEntry(
exchangeBaseUrl: string,
options: { forceUpdate?: boolean } = {},
): Promise<void> {
- const canonBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
-
logger.info(
- `starting update of exchange entry ${canonBaseUrl}, forced=${
+ `starting update of exchange entry ${exchangeBaseUrl}, forced=${
options.forceUpdate ?? false
}`,
);
const { notification } = await wex.db.runReadWriteTx(
- ["exchanges", "exchangeDetails"],
+ { storeNames: ["exchanges", "exchangeDetails"] },
async (tx) => {
+ wex.ws.exchangeCache.clear();
return provideExchangeRecordInTx(wex.ws, tx, exchangeBaseUrl);
},
);
@@ -913,9 +935,9 @@ async function startUpdateExchangeEntry(
const { oldExchangeState, newExchangeState, taskId } =
await wex.db.runReadWriteTx(
- ["exchanges", "operationRetries"],
+ { storeNames: ["exchanges", "operationRetries"] },
async (tx) => {
- const r = await tx.exchanges.get(canonBaseUrl);
+ const r = await tx.exchanges.get(exchangeBaseUrl);
if (!r) {
throw Error("exchange not found");
}
@@ -952,6 +974,7 @@ async function startUpdateExchangeEntry(
r.cachebreakNextUpdate = options.forceUpdate;
break;
}
+ wex.ws.exchangeCache.clear();
await tx.exchanges.put(r);
const newExchangeState = getExchangeState(r);
// Reset retries for updating the exchange entry.
@@ -962,7 +985,7 @@ async function startUpdateExchangeEntry(
);
wex.ws.notify({
type: NotificationType.ExchangeStateTransition,
- exchangeBaseUrl: canonBaseUrl,
+ exchangeBaseUrl,
newExchangeState: newExchangeState,
oldExchangeState: oldExchangeState,
});
@@ -1006,13 +1029,15 @@ async function internalWaitReadyExchange(
logger.info(`waiting for ready exchange ${canonUrl}`);
const { exchange, exchangeDetails, retryInfo, scopeInfo } =
await wex.db.runReadOnlyTx(
- [
- "exchanges",
- "exchangeDetails",
- "operationRetries",
- "globalCurrencyAuditors",
- "globalCurrencyExchanges",
- ],
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "operationRetries",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
async (tx) => {
const exchange = await tx.exchanges.get(canonUrl);
const exchangeDetails = await getExchangeRecordsInternal(
@@ -1046,6 +1071,14 @@ async function internalWaitReadyExchange(
ready = true;
}
break;
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,
+ {
+ exchangeBaseUrl: canonUrl,
+ innerError: retryInfo?.lastError,
+ },
+ );
default: {
if (retryInfo) {
throw TalerError.fromDetail(
@@ -1124,15 +1157,24 @@ export async function fetchFreshExchange(
expectedMasterPub?: string;
} = {},
): Promise<ReadyExchangeSummary> {
- const canonUrl = canonicalizeBaseUrl(baseUrl);
+ if (!options.forceUpdate) {
+ const cachedResp = wex.ws.exchangeCache.get(baseUrl);
+ if (cachedResp) {
+ return cachedResp;
+ }
+ } else {
+ wex.ws.exchangeCache.clear();
+ }
- wex.taskScheduler.ensureRunning();
+ await wex.taskScheduler.ensureRunning();
- await startUpdateExchangeEntry(wex, canonUrl, {
+ await startUpdateExchangeEntry(wex, baseUrl, {
forceUpdate: options.forceUpdate,
});
- return await waitReadyExchange(wex, canonUrl, options);
+ const resp = await waitReadyExchange(wex, baseUrl, options);
+ wex.ws.exchangeCache.put(baseUrl, resp);
+ return resp;
}
async function waitReadyExchange(
@@ -1245,10 +1287,9 @@ export async function updateExchangeFromUrlHandler(
exchangeBaseUrl: string,
): Promise<TaskRunResult> {
logger.trace(`updating exchange info for ${exchangeBaseUrl}`);
- exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
const oldExchangeRec = await wex.db.runReadOnlyTx(
- ["exchanges"],
+ { storeNames: ["exchanges"] },
async (tx) => {
return tx.exchanges.get(exchangeBaseUrl);
},
@@ -1270,9 +1311,11 @@ export async function updateExchangeFromUrlHandler(
return TaskRunResult.finished();
case ExchangeEntryDbUpdateStatus.InitialUpdate:
case ExchangeEntryDbUpdateStatus.ReadyUpdate:
- case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
updateRequestedExplicitly = true;
break;
+ case ExchangeEntryDbUpdateStatus.UnavailableUpdate:
+ // Only retry when scheduled to respect backoff
+ break;
case ExchangeEntryDbUpdateStatus.Ready:
break;
default:
@@ -1390,12 +1433,8 @@ export async function updateExchangeFromUrlHandler(
["text/plain"],
);
- let recoupGroupId: string | undefined;
-
logger.trace("updating exchange info in database");
- let detailsPointerChanged = false;
-
let ageMask = 0;
for (const x of keysInfo.currentDenominations) {
if (
@@ -1406,41 +1445,72 @@ export async function updateExchangeFromUrlHandler(
break;
}
}
-
- const now = AbsoluteTime.now();
let noFees = checkNoFees(keysInfo);
let peerPaymentsDisabled = checkPeerPaymentsDisabled(keysInfo);
const updated = await wex.db.runReadWriteTx(
- [
- "exchanges",
- "exchangeDetails",
- "exchangeSignKeys",
- "denominations",
- "coins",
- "refreshGroups",
- "recoupGroups",
- ],
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "exchangeSignKeys",
+ "denominations",
+ "coins",
+ "refreshGroups",
+ "recoupGroups",
+ "coinAvailability",
+ "denomLossEvents",
+ ],
+ },
async (tx) => {
const r = await tx.exchanges.get(exchangeBaseUrl);
if (!r) {
logger.warn(`exchange ${exchangeBaseUrl} no longer present`);
return;
}
+
+ wex.ws.refreshCostCache.clear();
+ wex.ws.exchangeCache.clear();
+ wex.ws.denomInfoCache.clear();
+
const oldExchangeState = getExchangeState(r);
const existingDetails = await getExchangeRecordsInternal(tx, r.baseUrl);
+ let detailsPointerChanged = false;
if (!existingDetails) {
detailsPointerChanged = true;
}
+ let detailsIncompatible = false;
if (existingDetails) {
if (existingDetails.masterPublicKey !== keysInfo.masterPublicKey) {
+ detailsIncompatible = true;
detailsPointerChanged = true;
}
if (existingDetails.currency !== keysInfo.currency) {
+ detailsIncompatible = true;
detailsPointerChanged = true;
}
// FIXME: We need to do some consistency checks!
}
+ if (detailsIncompatible) {
+ logger.warn(
+ `exchange ${r.baseUrl} has incompatible data in /keys, not updating`,
+ );
+ // We don't support this gracefully right now.
+ // See https://bugs.taler.net/n/8576
+ r.updateStatus = ExchangeEntryDbUpdateStatus.UnavailableUpdate;
+ r.updateRetryCounter = (r.updateRetryCounter ?? 0) + 1;
+ r.nextUpdateStamp = computeDbBackoff(r.updateRetryCounter);
+ r.nextRefreshCheckStamp = timestampPreciseToDb(
+ AbsoluteTime.toPreciseTimestamp(AbsoluteTime.never()),
+ );
+ r.cachebreakNextUpdate = true;
+ await tx.exchanges.put(r);
+ return {
+ oldExchangeState,
+ newExchangeState: getExchangeState(r),
+ };
+ }
+ r.updateRetryCounter = 0;
const newDetails: ExchangeDetailsRecord = {
auditors: keysInfo.auditors,
currency: keysInfo.currency,
@@ -1475,10 +1545,10 @@ export async function updateExchangeFromUrlHandler(
updateClock: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
}
+
r.updateStatus = ExchangeEntryDbUpdateStatus.Ready;
r.cachebreakNextUpdate = false;
await tx.exchanges.put(r);
- logger.info(`putting new exchange details in DB: ${j2s(newDetails)}`);
const drRowId = await tx.exchangeDetails.put(newDetails);
checkDbInvariant(typeof drRowId.key === "number");
@@ -1508,6 +1578,7 @@ export async function updateExchangeFromUrlHandler(
const currentDenomSet = new Set<string>(
keysInfo.currentDenominations.map((x) => x.denomPubHash),
);
+
for (const currentDenom of keysInfo.currentDenominations) {
const oldDenom = oldDenomByDph.get(currentDenom.denomPubHash);
if (oldDenom) {
@@ -1552,44 +1623,14 @@ export async function updateExchangeFromUrlHandler(
logger.trace("done updating denominations in database");
- // Handle recoup
- const recoupDenomList = keysInfo.recoup;
- const newlyRevokedCoinPubs: string[] = [];
- logger.trace("recoup list from exchange", recoupDenomList);
- for (const recoupInfo of recoupDenomList) {
- const oldDenom = await tx.denominations.get([
- r.baseUrl,
- recoupInfo.h_denom_pub,
- ]);
- if (!oldDenom) {
- // We never even knew about the revoked denomination, all good.
- continue;
- }
- if (oldDenom.isRevoked) {
- // We already marked the denomination as revoked,
- // this implies we revoked all coins
- logger.trace("denom already revoked");
- continue;
- }
- logger.info("revoking denom", recoupInfo.h_denom_pub);
- oldDenom.isRevoked = true;
- await tx.denominations.put(oldDenom);
- const affectedCoins = await tx.coins.indexes.byDenomPubHash
- .iter(recoupInfo.h_denom_pub)
- .toArray();
- for (const ac of affectedCoins) {
- newlyRevokedCoinPubs.push(ac.coinPub);
- }
- }
- if (newlyRevokedCoinPubs.length != 0) {
- logger.info("recouping coins", newlyRevokedCoinPubs);
- recoupGroupId = await createRecoupGroup(
- wex,
- tx,
- exchangeBaseUrl,
- newlyRevokedCoinPubs,
- );
- }
+ const denomLossResult = await handleDenomLoss(
+ wex,
+ tx,
+ newDetails.currency,
+ exchangeBaseUrl,
+ );
+
+ await handleRecoup(wex, tx, exchangeBaseUrl, keysInfo.recoup);
const newExchangeState = getExchangeState(r);
@@ -1598,24 +1639,21 @@ export async function updateExchangeFromUrlHandler(
exchangeDetails: newDetails,
oldExchangeState,
newExchangeState,
+ denomLossResult,
};
},
);
- if (recoupGroupId) {
- const recoupTaskId = constructTaskIdentifier({
- tag: PendingTaskType.Recoup,
- recoupGroupId,
- });
- // Asynchronously start recoup. This doesn't need to finish
- // for the exchange update to be considered finished.
- wex.taskScheduler.startShepherdTask(recoupTaskId);
- }
-
if (!updated) {
throw Error("something went wrong with updating the exchange");
}
+ if (updated.denomLossResult) {
+ for (const notif of updated.denomLossResult.notifications) {
+ wex.ws.notify(notif);
+ }
+ }
+
logger.trace("done updating exchange info in database");
logger.trace(`doing auto-refresh check for '${exchangeBaseUrl}'`);
@@ -1628,13 +1666,16 @@ export async function updateExchangeFromUrlHandler(
if (refreshCheckNecessary) {
// Do auto-refresh.
await wex.db.runReadWriteTx(
- [
- "coins",
- "denominations",
- "coinAvailability",
- "refreshGroups",
- "exchanges",
- ],
+ {
+ storeNames: [
+ "coins",
+ "denominations",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ "exchanges",
+ ],
+ },
async (tx) => {
const exchange = await tx.exchanges.get(exchangeBaseUrl);
if (!exchange || !exchange.detailsPointer) {
@@ -1692,6 +1733,7 @@ export async function updateExchangeFromUrlHandler(
exchange.nextRefreshCheckStamp = timestampPreciseToDb(
AbsoluteTime.toPreciseTimestamp(minCheckThreshold),
);
+ wex.ws.exchangeCache.clear();
await tx.exchanges.put(exchange);
},
);
@@ -1709,6 +1751,299 @@ export async function updateExchangeFromUrlHandler(
return TaskRunResult.progress();
}
+interface DenomLossResult {
+ notifications: WalletNotification[];
+}
+
+async function handleDenomLoss(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["coinAvailability", "denominations", "denomLossEvents", "coins"]
+ >,
+ currency: string,
+ exchangeBaseUrl: string,
+): Promise<DenomLossResult> {
+ const coinAvailabilityRecs =
+ await tx.coinAvailability.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl);
+ const denomsVanished: string[] = [];
+ const denomsUnoffered: string[] = [];
+ const denomsExpired: string[] = [];
+ let amountVanished = Amount.zeroOfCurrency(currency);
+ let amountExpired = Amount.zeroOfCurrency(currency);
+ let amountUnoffered = Amount.zeroOfCurrency(currency);
+
+ const result: DenomLossResult = {
+ notifications: [],
+ };
+
+ for (const coinAv of coinAvailabilityRecs) {
+ if (coinAv.freshCoinCount <= 0) {
+ continue;
+ }
+ const n = coinAv.freshCoinCount;
+ const denom = await tx.denominations.get([
+ coinAv.exchangeBaseUrl,
+ coinAv.denomPubHash,
+ ]);
+ const timestampExpireDeposit = !denom
+ ? undefined
+ : timestampAbsoluteFromDb(denom.stampExpireDeposit);
+ if (!denom) {
+ // Remove availability
+ coinAv.freshCoinCount = 0;
+ coinAv.visibleCoinCount = 0;
+ await tx.coinAvailability.put(coinAv);
+ denomsVanished.push(coinAv.denomPubHash);
+ const total = Amount.from(coinAv.value).mult(n);
+ amountVanished = amountVanished.add(total);
+ } else if (!denom.isOffered) {
+ // Remove availability
+ coinAv.freshCoinCount = 0;
+ coinAv.visibleCoinCount = 0;
+ await tx.coinAvailability.put(coinAv);
+ denomsUnoffered.push(coinAv.denomPubHash);
+ const total = Amount.from(coinAv.value).mult(n);
+ amountUnoffered = amountUnoffered.add(total);
+ } else if (
+ timestampExpireDeposit &&
+ AbsoluteTime.isExpired(timestampExpireDeposit)
+ ) {
+ // Remove availability
+ coinAv.freshCoinCount = 0;
+ coinAv.visibleCoinCount = 0;
+ await tx.coinAvailability.put(coinAv);
+ denomsExpired.push(coinAv.denomPubHash);
+ const total = Amount.from(coinAv.value).mult(n);
+ amountExpired = amountExpired.add(total);
+ } else {
+ // Denomination is still fine!
+ continue;
+ }
+
+ logger.warn(`denomination ${coinAv.denomPubHash} is a loss`);
+
+ const coins = await tx.coins.indexes.byDenomPubHash.getAll(
+ coinAv.denomPubHash,
+ );
+ for (const coin of coins) {
+ switch (coin.status) {
+ case CoinStatus.Fresh:
+ case CoinStatus.FreshSuspended: {
+ coin.status = CoinStatus.DenomLoss;
+ await tx.coins.put(coin);
+ break;
+ }
+ }
+ }
+ }
+
+ if (denomsVanished.length > 0) {
+ const denomLossEventId = encodeCrock(getRandomBytes(32));
+ await tx.denomLossEvents.add({
+ denomLossEventId,
+ amount: amountVanished.toString(),
+ currency,
+ exchangeBaseUrl,
+ denomPubHashes: denomsVanished,
+ eventType: DenomLossEventType.DenomVanished,
+ status: DenomLossStatus.Done,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId,
+ });
+ result.notifications.push({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: {
+ major: TransactionMajorState.None,
+ },
+ newTxState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+ result.notifications.push({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ }
+
+ if (denomsUnoffered.length > 0) {
+ const denomLossEventId = encodeCrock(getRandomBytes(32));
+ await tx.denomLossEvents.add({
+ denomLossEventId,
+ amount: amountUnoffered.toString(),
+ currency,
+ exchangeBaseUrl,
+ denomPubHashes: denomsUnoffered,
+ eventType: DenomLossEventType.DenomUnoffered,
+ status: DenomLossStatus.Done,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId,
+ });
+ result.notifications.push({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: {
+ major: TransactionMajorState.None,
+ },
+ newTxState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+ result.notifications.push({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ }
+
+ if (denomsExpired.length > 0) {
+ const denomLossEventId = encodeCrock(getRandomBytes(32));
+ await tx.denomLossEvents.add({
+ denomLossEventId,
+ amount: amountExpired.toString(),
+ currency,
+ exchangeBaseUrl,
+ denomPubHashes: denomsUnoffered,
+ eventType: DenomLossEventType.DenomExpired,
+ status: DenomLossStatus.Done,
+ timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId,
+ });
+ result.notifications.push({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: {
+ major: TransactionMajorState.None,
+ },
+ newTxState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+ result.notifications.push({
+ type: NotificationType.BalanceChange,
+ hintTransactionId: transactionId,
+ });
+ }
+
+ return result;
+}
+
+export function computeDenomLossTransactionStatus(
+ rec: DenomLossEventRecord,
+): TransactionState {
+ switch (rec.status) {
+ case DenomLossStatus.Aborted:
+ return {
+ major: TransactionMajorState.Aborted,
+ };
+ case DenomLossStatus.Done:
+ return {
+ major: TransactionMajorState.Done,
+ };
+ }
+}
+
+export class DenomLossTransactionContext implements TransactionContext {
+ get taskId(): TaskIdStr | undefined {
+ return undefined;
+ }
+ transactionId: TransactionIdStr;
+
+ abortTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ suspendTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ resumeTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ failTransaction(): Promise<void> {
+ throw new Error("Method not implemented.");
+ }
+ async deleteTransaction(): Promise<void> {
+ const transitionInfo = await this.wex.db.runReadWriteTx(
+ { storeNames: ["denomLossEvents"] },
+ async (tx) => {
+ const rec = await tx.denomLossEvents.get(this.denomLossEventId);
+ if (rec) {
+ const oldTxState = computeDenomLossTransactionStatus(rec);
+ await tx.denomLossEvents.delete(this.denomLossEventId);
+ return {
+ oldTxState,
+ newTxState: {
+ major: TransactionMajorState.Deleted,
+ },
+ };
+ }
+ return undefined;
+ },
+ );
+ notifyTransition(this.wex, this.transactionId, transitionInfo);
+ }
+
+ constructor(
+ private wex: WalletExecutionContext,
+ public denomLossEventId: string,
+ ) {
+ this.transactionId = constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId,
+ });
+ }
+}
+
+async function handleRecoup(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["denominations", "coins", "recoupGroups", "refreshGroups"]
+ >,
+ exchangeBaseUrl: string,
+ recoup: Recoup[],
+): Promise<void> {
+ // Handle recoup
+ const recoupDenomList = recoup;
+ const newlyRevokedCoinPubs: string[] = [];
+ logger.trace("recoup list from exchange", recoupDenomList);
+ for (const recoupInfo of recoupDenomList) {
+ const oldDenom = await tx.denominations.get([
+ exchangeBaseUrl,
+ recoupInfo.h_denom_pub,
+ ]);
+ if (!oldDenom) {
+ // We never even knew about the revoked denomination, all good.
+ continue;
+ }
+ if (oldDenom.isRevoked) {
+ // We already marked the denomination as revoked,
+ // this implies we revoked all coins
+ logger.trace("denom already revoked");
+ continue;
+ }
+ logger.info("revoking denom", recoupInfo.h_denom_pub);
+ oldDenom.isRevoked = true;
+ await tx.denominations.put(oldDenom);
+ const affectedCoins = await tx.coins.indexes.byDenomPubHash.getAll(
+ recoupInfo.h_denom_pub,
+ );
+ for (const ac of affectedCoins) {
+ newlyRevokedCoinPubs.push(ac.coinPub);
+ }
+ }
+ if (newlyRevokedCoinPubs.length != 0) {
+ logger.info("recouping coins", newlyRevokedCoinPubs);
+ await createRecoupGroup(wex, tx, exchangeBaseUrl, newlyRevokedCoinPubs);
+ }
+}
+
function getAutoRefreshExecuteThresholdForDenom(
d: DenominationRecord,
): AbsoluteTime {
@@ -1747,7 +2082,7 @@ export async function getExchangePaytoUri(
// We do the update here, since the exchange might not even exist
// yet in our database.
const details = await wex.db.runReadOnlyTx(
- ["exchanges", "exchangeDetails"],
+ { storeNames: ["exchanges", "exchangeDetails"] },
async (tx) => {
return getExchangeRecordsInternal(tx, exchangeBaseUrl);
},
@@ -1789,10 +2124,11 @@ export async function getExchangeTos(
acceptLanguage,
);
- await wex.db.runReadWriteTx(["exchanges"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["exchanges"] }, async (tx) => {
const updateExchangeEntry = await tx.exchanges.get(exchangeBaseUrl);
if (updateExchangeEntry) {
updateExchangeEntry.tosCurrentEtag = tosDownload.tosEtag;
+ wex.ws.exchangeCache.clear();
await tx.exchanges.put(updateExchangeEntry);
}
});
@@ -1845,13 +2181,15 @@ export async function listExchanges(
): Promise<ExchangesListResponse> {
const exchanges: ExchangeListItem[] = [];
await wex.db.runReadOnlyTx(
- [
- "exchanges",
- "operationRetries",
- "exchangeDetails",
- "globalCurrencyAuditors",
- "globalCurrencyExchanges",
- ],
+ {
+ storeNames: [
+ "exchanges",
+ "operationRetries",
+ "exchangeDetails",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ],
+ },
async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) {
@@ -1888,7 +2226,6 @@ export async function markExchangeUsed(
tx: WalletDbReadWriteTransaction<["exchanges"]>,
exchangeBaseUrl: string,
): Promise<{ notif: WalletNotification | undefined }> {
- exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
logger.info(`marking exchange ${exchangeBaseUrl} as used`);
const exch = await tx.exchanges.get(exchangeBaseUrl);
if (!exch) {
@@ -1928,7 +2265,7 @@ export async function getExchangeDetailedInfo(
exchangeBaseurl: string,
): Promise<ExchangeDetailedResponse> {
const exchange = await wex.db.runReadOnlyTx(
- ["exchanges", "exchangeDetails", "denominations"],
+ { storeNames: ["exchanges", "exchangeDetails", "denominations"] },
async (tx) => {
const ex = await tx.exchanges.get(exchangeBaseurl);
const dp = ex?.detailsPointer;
@@ -2184,19 +2521,21 @@ export async function deleteExchange(
req: DeleteExchangeRequest,
): Promise<void> {
let inUse: boolean = false;
- const exchangeBaseUrl = canonicalizeBaseUrl(req.exchangeBaseUrl);
+ const exchangeBaseUrl = req.exchangeBaseUrl;
await wex.db.runReadWriteTx(
- [
- "exchanges",
- "exchangeDetails",
- "transactions",
- "coinAvailability",
- "coins",
- "denominations",
- "exchangeSignKeys",
- "withdrawalGroups",
- "planchets",
- ],
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "transactions",
+ "coinAvailability",
+ "coins",
+ "denominations",
+ "exchangeSignKeys",
+ "withdrawalGroups",
+ "planchets",
+ ],
+ },
async (tx) => {
const exchangeRec = await tx.exchanges.get(exchangeBaseUrl);
if (!exchangeRec) {
@@ -2210,6 +2549,7 @@ export async function deleteExchange(
return;
}
await purgeExchange(tx, exchangeBaseUrl);
+ wex.ws.exchangeCache.clear();
},
);
@@ -2227,7 +2567,7 @@ export async function getExchangeResources(
): Promise<GetExchangeResourcesResponse> {
// Withdrawals include internal withdrawals from peer transactions
const res = await wex.db.runReadOnlyTx(
- ["exchanges", "withdrawalGroups", "coins"],
+ { storeNames: ["exchanges", "withdrawalGroups", "coins"] },
async (tx) => {
const exchangeRecord = await tx.exchanges.get(exchangeBaseUrl);
if (!exchangeRecord) {
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts
index 63ccb8b56..1f7d95959 100644
--- a/packages/taler-wallet-core/src/instructedAmountConversion.ts
+++ b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -150,7 +150,14 @@ async function getAvailableDenoms(
const operationType = getOperationType(TransactionType.Deposit);
return await wex.db.runReadOnlyTx(
- ["exchanges", "exchangeDetails", "denominations", "coinAvailability"],
+ {
+ storeNames: [
+ "exchanges",
+ "exchangeDetails",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
async (tx) => {
const list: CoinInfo[] = [];
const exchanges: Record<string, ExchangeInfo> = {};
diff --git a/packages/taler-wallet-core/src/observable-wrappers.ts b/packages/taler-wallet-core/src/observable-wrappers.ts
index b36f41611..717de41ca 100644
--- a/packages/taler-wallet-core/src/observable-wrappers.ts
+++ b/packages/taler-wallet-core/src/observable-wrappers.ts
@@ -25,7 +25,6 @@ import { IDBDatabase } from "@gnu-taler/idb-bridge";
import {
ObservabilityContext,
ObservabilityEventType,
- RetryLoopOpts,
} from "@gnu-taler/taler-util";
import { TaskIdStr } from "./common.js";
import { TalerCryptoInterface } from "./index.js";
@@ -61,17 +60,22 @@ export class ObservableTaskScheduler implements TaskScheduler {
}
}
+ shutdown(): Promise<void> {
+ return this.impl.shutdown();
+ }
+
getActiveTasks(): TaskIdStr[] {
return this.impl.getActiveTasks();
}
- ensureRunning(): void {
- return this.impl.ensureRunning();
+ isIdle(): boolean {
+ return this.impl.isIdle();
}
- run(opts?: RetryLoopOpts | undefined): Promise<void> {
- return this.impl.run(opts);
+ ensureRunning(): Promise<void> {
+ return this.impl.ensureRunning();
}
+
startShepherdTask(taskId: TaskIdStr): void {
this.declareDep(taskId);
this.oc.observe({
@@ -80,6 +84,7 @@ export class ObservableTaskScheduler implements TaskScheduler {
});
return this.impl.startShepherdTask(taskId);
}
+
stopShepherdTask(taskId: TaskIdStr): void {
this.declareDep(taskId);
this.oc.observe({
@@ -88,6 +93,7 @@ export class ObservableTaskScheduler implements TaskScheduler {
});
return this.impl.stopShepherdTask(taskId);
}
+
resetTaskRetries(taskId: TaskIdStr): Promise<void> {
this.declareDep(taskId);
if (this.taskDepCache.size > 500) {
@@ -99,7 +105,8 @@ export class ObservableTaskScheduler implements TaskScheduler {
});
return this.impl.resetTaskRetries(taskId);
}
- reload(): void {
+
+ async reload(): Promise<void> {
return this.impl.reload();
}
}
@@ -170,21 +177,21 @@ export class ObservableDbAccess<StoreMap> implements DbAccess<StoreMap> {
const location = getCallerInfo();
this.oc.observe({
type: ObservabilityEventType.DbQueryStart,
- name: "<unknown>",
+ name: options.label ?? "<unknown>",
location,
});
try {
const ret = await this.impl.runAllStoresReadOnlyTx(options, txf);
this.oc.observe({
type: ObservabilityEventType.DbQueryFinishSuccess,
- name: "<unknown>",
+ name: options.label ?? "<unknown>",
location,
});
return ret;
} catch (e) {
this.oc.observe({
type: ObservabilityEventType.DbQueryFinishError,
- name: "<unknown>",
+ name: options.label ?? "<unknown>",
location,
});
throw e;
@@ -192,27 +199,30 @@ export class ObservableDbAccess<StoreMap> implements DbAccess<StoreMap> {
}
async runReadWriteTx<T, StoreNameArray extends StoreNames<StoreMap>[]>(
- storeNames: StoreNameArray,
+ opts: {
+ storeNames: StoreNameArray;
+ label?: string;
+ },
txf: (tx: DbReadWriteTransaction<StoreMap, StoreNameArray>) => Promise<T>,
): Promise<T> {
const location = getCallerInfo();
this.oc.observe({
type: ObservabilityEventType.DbQueryStart,
- name: "<unknown>",
+ name: opts.label ?? "<unknown>",
location,
});
try {
- const ret = await this.impl.runReadWriteTx(storeNames, txf);
+ const ret = await this.impl.runReadWriteTx(opts, txf);
this.oc.observe({
type: ObservabilityEventType.DbQueryFinishSuccess,
- name: "<unknown>",
+ name: opts.label ?? "<unknown>",
location,
});
return ret;
} catch (e) {
this.oc.observe({
type: ObservabilityEventType.DbQueryFinishError,
- name: "<unknown>",
+ name: opts.label ?? "<unknown>",
location,
});
throw e;
@@ -220,27 +230,30 @@ export class ObservableDbAccess<StoreMap> implements DbAccess<StoreMap> {
}
async runReadOnlyTx<T, StoreNameArray extends StoreNames<StoreMap>[]>(
- storeNames: StoreNameArray,
+ opts: {
+ storeNames: StoreNameArray;
+ label?: string;
+ },
txf: (tx: DbReadOnlyTransaction<StoreMap, StoreNameArray>) => Promise<T>,
): Promise<T> {
const location = getCallerInfo();
try {
this.oc.observe({
type: ObservabilityEventType.DbQueryStart,
- name: "<unknown>",
+ name: opts.label ?? "<unknown>",
location,
});
- const ret = await this.impl.runReadOnlyTx(storeNames, txf);
+ const ret = await this.impl.runReadOnlyTx(opts, txf);
this.oc.observe({
type: ObservabilityEventType.DbQueryFinishSuccess,
- name: "<unknown>",
+ name: opts.label ?? "<unknown>",
location,
});
return ret;
} catch (e) {
this.oc.observe({
type: ObservabilityEventType.DbQueryFinishError,
- name: "<unknown>",
+ name: opts.label ?? "<unknown>",
location,
});
throw e;
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
index 812d32429..49ebc282e 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -63,12 +63,12 @@ import {
parsePayTemplateUri,
parsePayUri,
parseTalerUri,
- PayCoinSelection,
PreparePayResult,
PreparePayResultType,
PreparePayTemplateRequest,
randomBytes,
RefreshReason,
+ SelectedProspectiveCoin,
SharePaymentResult,
StartRefundQueryForUriResponse,
stringifyPayUri,
@@ -143,7 +143,6 @@ import {
getDenomInfo,
WalletExecutionContext,
} from "./wallet.js";
-import { getCandidateWithdrawalDenomsTx } from "./withdraw.js";
/**
* Logger.
@@ -201,7 +200,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
const ws = this.wex;
const extraStores = opts.extraStores ?? [];
const transitionInfo = await ws.db.runReadWriteTx(
- ["purchases", ...extraStores],
+ { storeNames: ["purchases", ...extraStores] },
async (tx) => {
const purchaseRec = await tx.purchases.get(this.proposalId);
if (!purchaseRec) {
@@ -228,26 +227,29 @@ export class PayMerchantTransactionContext implements TransactionContext {
async deleteTransaction(): Promise<void> {
const { wex: ws, proposalId } = this;
- await ws.db.runReadWriteTx(["purchases", "tombstones"], async (tx) => {
- let found = false;
- const purchase = await tx.purchases.get(proposalId);
- if (purchase) {
- found = true;
- await tx.purchases.delete(proposalId);
- }
- if (found) {
- await tx.tombstones.put({
- id: TombstoneTag.DeletePayment + ":" + proposalId,
- });
- }
- });
+ await ws.db.runReadWriteTx(
+ { storeNames: ["purchases", "tombstones"] },
+ async (tx) => {
+ let found = false;
+ const purchase = await tx.purchases.get(proposalId);
+ if (purchase) {
+ found = true;
+ await tx.purchases.delete(proposalId);
+ }
+ if (found) {
+ await tx.tombstones.put({
+ id: TombstoneTag.DeletePayment + ":" + proposalId,
+ });
+ }
+ },
+ );
}
async suspendTransaction(): Promise<void> {
const { wex, proposalId, transactionId } = this;
wex.taskScheduler.stopShepherdTask(this.taskId);
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) {
@@ -269,14 +271,17 @@ export class PayMerchantTransactionContext implements TransactionContext {
async abortTransaction(): Promise<void> {
const { wex, proposalId, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- [
- "purchases",
- "refreshGroups",
- "denominations",
- "coinAvailability",
- "coins",
- "operationRetries",
- ],
+ {
+ storeNames: [
+ "purchases",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ "operationRetries",
+ ],
+ },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) {
@@ -290,7 +295,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
case PurchaseStatus.PendingPaying:
case PurchaseStatus.SuspendedPaying: {
purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
- if (purchase.payInfo) {
+ if (purchase.payInfo && purchase.payInfo.payCoinSelection) {
const coinSel = purchase.payInfo.payCoinSelection;
const currency = Amounts.currencyOf(
purchase.payInfo.totalPayCost,
@@ -344,7 +349,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
async resumeTransaction(): Promise<void> {
const { wex, proposalId, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) {
@@ -367,14 +372,16 @@ export class PayMerchantTransactionContext implements TransactionContext {
async failTransaction(): Promise<void> {
const { wex, proposalId, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- [
- "purchases",
- "refreshGroups",
- "denominations",
- "coinAvailability",
- "coins",
- "operationRetries",
- ],
+ {
+ storeNames: [
+ "purchases",
+ "refreshGroups",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ "operationRetries",
+ ],
+ },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) {
@@ -415,15 +422,18 @@ export class RefundTransactionContext implements TransactionContext {
async deleteTransaction(): Promise<void> {
const { wex, refundGroupId, transactionId } = this;
- await wex.db.runReadWriteTx(["refundGroups", "tombstones"], async (tx) => {
- const refundRecord = await tx.refundGroups.get(refundGroupId);
- if (!refundRecord) {
- return;
- }
- await tx.refundGroups.delete(refundGroupId);
- await tx.tombstones.put({ id: transactionId });
- // FIXME: Also tombstone the refund items, so that they won't reappear.
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["refundGroups", "tombstones"] },
+ async (tx) => {
+ const refundRecord = await tx.refundGroups.get(refundGroupId);
+ if (!refundRecord) {
+ return;
+ }
+ await tx.refundGroups.delete(refundGroupId);
+ await tx.tombstones.put({ id: transactionId });
+ // FIXME: Also tombstone the refund items, so that they won't reappear.
+ },
+ );
}
suspendTransaction(): Promise<void> {
@@ -452,47 +462,37 @@ export class RefundTransactionContext implements TransactionContext {
*/
export async function getTotalPaymentCost(
wex: WalletExecutionContext,
- pcs: PayCoinSelection,
+ currency: string,
+ pcs: SelectedProspectiveCoin[],
): Promise<AmountJson> {
- const currency = Amounts.currencyOf(pcs.customerDepositFees);
- return wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
- const costs: AmountJson[] = [];
- for (let i = 0; i < pcs.coins.length; i++) {
- const coin = await tx.coins.get(pcs.coins[i].coinPub);
- if (!coin) {
- throw Error("can't calculate payment cost, coin not found");
- }
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- if (!denom) {
- throw Error(
- "can't calculate payment cost, denomination for coin not found",
+ return wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations"] },
+ async (tx) => {
+ const costs: AmountJson[] = [];
+ for (let i = 0; i < pcs.length; i++) {
+ const denom = await tx.denominations.get([
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
+ ]);
+ if (!denom) {
+ throw Error(
+ "can't calculate payment cost, denomination for coin not found",
+ );
+ }
+ const amountLeft = Amounts.sub(denom.value, pcs[i].contribution).amount;
+ const refreshCost = await getTotalRefreshCost(
+ wex,
+ tx,
+ DenominationRecord.toDenomInfo(denom),
+ amountLeft,
);
+ costs.push(Amounts.parseOrThrow(pcs[i].contribution));
+ costs.push(refreshCost);
}
- const allDenoms = await getCandidateWithdrawalDenomsTx(
- wex,
- tx,
- coin.exchangeBaseUrl,
- currency,
- );
- const amountLeft = Amounts.sub(
- denom.value,
- pcs.coins[i].contribution,
- ).amount;
- const refreshCost = getTotalRefreshCost(
- allDenoms,
- DenominationRecord.toDenomInfo(denom),
- amountLeft,
- wex.ws.config.testing.denomselAllowLate,
- );
- costs.push(Amounts.parseOrThrow(pcs.coins[i].contribution));
- costs.push(refreshCost);
- }
- const zero = Amounts.zeroOfAmount(pcs.customerDepositFees);
- return Amounts.sum([zero, ...costs]).amount;
- });
+ const zero = Amounts.zeroOfCurrency(currency);
+ return Amounts.sum([zero, ...costs]).amount;
+ },
+ );
}
async function failProposalPermanently(
@@ -505,7 +505,7 @@ async function failProposalPermanently(
proposalId,
});
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
@@ -525,7 +525,7 @@ async function failProposalPermanently(
function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
return Duration.multiply(
{ d_ms: 15000 },
- 1 + (purchase.payInfo?.payCoinSelection.coinPubs.length ?? 0) / 5,
+ 1 + (purchase.payInfo?.payCoinSelection?.coinPubs.length ?? 0) / 5,
);
}
@@ -567,7 +567,10 @@ export async function expectProposalDownload(
if (parentTx) {
return getFromTransaction(parentTx);
}
- return await wex.db.runReadOnlyTx(["contractTerms"], getFromTransaction);
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["contractTerms"] },
+ getFromTransaction,
+ );
}
export function extractContractData(
@@ -606,9 +609,12 @@ async function processDownloadProposal(
wex: WalletExecutionContext,
proposalId: string,
): Promise<TaskRunResult> {
- const proposal = await wex.db.runReadOnlyTx(["purchases"], async (tx) => {
- return await tx.purchases.get(proposalId);
- });
+ const proposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return await tx.purchases.get(proposalId);
+ },
+ );
if (!proposal) {
return TaskRunResult.finished();
@@ -779,7 +785,7 @@ async function processDownloadProposal(
logger.trace(`extracted contract data: ${j2s(contractData)}`);
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases", "contractTerms"],
+ { storeNames: ["purchases", "contractTerms"] },
async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
@@ -852,12 +858,15 @@ async function createOrReusePurchase(
claimToken: string | undefined,
noncePriv: string | undefined,
): Promise<string> {
- const oldProposals = await wex.db.runReadOnlyTx(["purchases"], async (tx) => {
- return tx.purchases.indexes.byUrlAndOrderId.getAll([
- merchantBaseUrl,
- orderId,
- ]);
- });
+ const oldProposals = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.indexes.byUrlAndOrderId.getAll([
+ merchantBaseUrl,
+ orderId,
+ ]);
+ },
+ );
const oldProposal = oldProposals.find((p) => {
return (
@@ -891,7 +900,7 @@ async function createOrReusePurchase(
// if this transaction was shared and the order is paid then it
// means that another wallet already paid the proposal
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const p = await tx.purchases.get(oldProposal.proposalId);
if (!p) {
@@ -957,7 +966,7 @@ async function createOrReusePurchase(
};
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
await tx.purchases.put(proposalRecord);
const oldTxState: TransactionState = {
@@ -991,7 +1000,7 @@ async function storeFirstPaySuccess(
});
const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
const transitionInfo = await wex.db.runReadWriteTx(
- ["contractTerms", "purchases"],
+ { storeNames: ["contractTerms", "purchases"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
@@ -1055,7 +1064,7 @@ async function storePayReplaySuccess(
proposalId,
});
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
@@ -1098,9 +1107,12 @@ async function handleInsufficientFunds(
): Promise<void> {
logger.trace("handling insufficient funds, trying to re-select coins");
- const proposal = await wex.db.runReadOnlyTx(["purchases"], async (tx) => {
- return tx.purchases.get(proposalId);
- });
+ const proposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
if (!proposal) {
return;
}
@@ -1136,17 +1148,23 @@ async function handleInsufficientFunds(
}
const payCoinSelection = payInfo.payCoinSelection;
+ if (!payCoinSelection) {
+ return;
+ }
- await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
- for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
- const coinPub = payCoinSelection.coinPubs[i];
- const contrib = payCoinSelection.coinContributions[i];
- prevPayCoins.push({
- coinPub,
- contribution: Amounts.parseOrThrow(contrib),
- });
- }
- });
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations"] },
+ async (tx) => {
+ for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
+ const coinPub = payCoinSelection.coinPubs[i];
+ const contrib = payCoinSelection.coinContributions[i];
+ prevPayCoins.push({
+ coinPub,
+ contribution: Amounts.parseOrThrow(contrib),
+ });
+ }
+ },
+ );
const res = await selectPayCoins(wex, {
restrictExchanges: {
@@ -1156,26 +1174,35 @@ async function handleInsufficientFunds(
restrictWireMethod: contractData.wireMethod,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- wireFeeAmortization: 1, // FIXME #8653
prevPayCoins,
requiredMinimumAge: contractData.minimumAge,
});
- if (res.type !== "success") {
- logger.trace("insufficient funds for coin re-selection");
- return;
+ switch (res.type) {
+ case "failure":
+ logger.trace("insufficient funds for coin re-selection");
+ return;
+ case "prospective":
+ return;
+ case "success":
+ break;
+ default:
+ assertUnreachable(res);
}
logger.trace("re-selected coins");
await wex.db.runReadWriteTx(
- [
- "purchases",
- "coins",
- "coinAvailability",
- "denominations",
- "refreshGroups",
- ],
+ {
+ storeNames: [
+ "purchases",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ ],
+ },
async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
@@ -1224,9 +1251,12 @@ async function checkPaymentByProposalId(
proposalId: string,
sessionId?: string,
): Promise<PreparePayResult> {
- let proposal = await wex.db.runReadOnlyTx(["purchases"], async (tx) => {
- return tx.purchases.get(proposalId);
- });
+ let proposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
if (!proposal) {
throw Error(`could not get proposal ${proposalId}`);
}
@@ -1235,7 +1265,7 @@ async function checkPaymentByProposalId(
if (existingProposalId) {
logger.trace("using existing purchase for same product");
const oldProposal = await wex.db.runReadOnlyTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
return tx.purchases.get(existingProposalId);
},
@@ -1254,6 +1284,8 @@ async function checkPaymentByProposalId(
proposalId = proposal.proposalId;
+ const currency = Amounts.currencyOf(contractData.amount);
+
const ctx = new PayMerchantTransactionContext(wex, proposalId);
const transactionId = ctx.transactionId;
@@ -1267,9 +1299,12 @@ async function checkPaymentByProposalId(
});
// First check if we already paid for it.
- const purchase = await wex.db.runReadOnlyTx(["purchases"], async (tx) => {
- return tx.purchases.get(proposalId);
- });
+ const purchase = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
if (
!purchase ||
@@ -1285,29 +1320,42 @@ async function checkPaymentByProposalId(
},
contractTermsAmount: instructedAmount,
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- wireFeeAmortization: 1, // FIXME #8653
prevPayCoins: [],
requiredMinimumAge: contractData.minimumAge,
restrictWireMethod: contractData.wireMethod,
});
- if (res.type !== "success") {
- logger.info("not allowing payment, insufficient coins");
- logger.info(
- `insufficient balance details: ${j2s(res.insufficientBalanceDetails)}`,
- );
- return {
- status: PreparePayResultType.InsufficientBalance,
- contractTerms: d.contractTermsRaw,
- proposalId: proposal.proposalId,
- transactionId,
- amountRaw: Amounts.stringify(d.contractData.amount),
- talerUri,
- balanceDetails: res.insufficientBalanceDetails,
- };
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (res.type) {
+ case "failure": {
+ logger.info("not allowing payment, insufficient coins");
+ logger.info(
+ `insufficient balance details: ${j2s(
+ res.insufficientBalanceDetails,
+ )}`,
+ );
+ return {
+ status: PreparePayResultType.InsufficientBalance,
+ contractTerms: d.contractTermsRaw,
+ proposalId: proposal.proposalId,
+ transactionId,
+ amountRaw: Amounts.stringify(d.contractData.amount),
+ talerUri,
+ balanceDetails: res.insufficientBalanceDetails,
+ };
+ }
+ case "prospective":
+ coins = res.result.prospectiveCoins;
+ break;
+ case "success":
+ coins = res.coinSel.coins;
+ break;
+ default:
+ assertUnreachable(res);
}
- const totalCost = await getTotalPaymentCost(wex, res.coinSel);
+ const totalCost = await getTotalPaymentCost(wex, currency, coins);
logger.trace("costInfo", totalCost);
logger.trace("coinsForPayment", res);
@@ -1332,7 +1380,7 @@ async function checkPaymentByProposalId(
);
logger.trace(`last: ${purchase.lastSessionId}, current: ${sessionId}`);
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
@@ -1409,9 +1457,12 @@ export async function getContractTermsDetails(
wex: WalletExecutionContext,
proposalId: string,
): Promise<WalletContractData> {
- const proposal = await wex.db.runReadOnlyTx(["purchases"], async (tx) => {
- return tx.purchases.get(proposalId);
- });
+ const proposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
if (!proposal) {
throw Error(`proposal with id ${proposalId} not found`);
@@ -1501,7 +1552,7 @@ async function internalWaitProposalDownloaded(
): Promise<void> {
while (true) {
const { purchase, retryInfo } = await ctx.wex.db.runReadOnlyTx(
- ["purchases", "operationRetries"],
+ { storeNames: ["purchases", "operationRetries"] },
async (tx) => {
return {
purchase: await tx.purchases.get(ctx.proposalId),
@@ -1598,24 +1649,27 @@ export async function generateDepositPermissions(
coin: CoinRecord;
denom: DenominationRecord;
}> = [];
- await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
- for (let i = 0; i < payCoinSel.coinContributions.length; i++) {
- const coin = await tx.coins.get(payCoinSel.coinPubs[i]);
- if (!coin) {
- throw Error("can't pay, allocated coin not found anymore");
- }
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
- if (!denom) {
- throw Error(
- "can't pay, denomination of allocated coin not found anymore",
- );
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations"] },
+ async (tx) => {
+ for (let i = 0; i < payCoinSel.coinContributions.length; i++) {
+ const coin = await tx.coins.get(payCoinSel.coinPubs[i]);
+ if (!coin) {
+ throw Error("can't pay, allocated coin not found anymore");
+ }
+ const denom = await tx.denominations.get([
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ ]);
+ if (!denom) {
+ throw Error(
+ "can't pay, denomination of allocated coin not found anymore",
+ );
+ }
+ coinWithDenom.push({ coin, denom });
}
- coinWithDenom.push({ coin, denom });
- }
- });
+ },
+ );
for (let i = 0; i < payCoinSel.coinContributions.length; i++) {
const { coin, denom } = coinWithDenom[i];
@@ -1650,7 +1704,7 @@ async function internalWaitPaymentResult(
): Promise<ConfirmPayResult> {
while (true) {
const txRes = await ctx.wex.db.runReadOnlyTx(
- ["purchases", "operationRetries"],
+ { storeNames: ["purchases", "operationRetries"] },
async (tx) => {
const purchase = await tx.purchases.get(ctx.proposalId);
const retryRecord = await tx.operationRetries.get(ctx.taskId);
@@ -1764,9 +1818,12 @@ export async function confirmPay(
logger.trace(
`executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`,
);
- const proposal = await wex.db.runReadOnlyTx(["purchases"], async (tx) => {
- return tx.purchases.get(proposalId);
- });
+ const proposal = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
if (!proposal) {
throw Error(`proposal with id ${proposalId} not found`);
@@ -1778,7 +1835,7 @@ export async function confirmPay(
}
const existingPurchase = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (
@@ -1811,6 +1868,8 @@ export async function confirmPay(
const contractData = d.contractData;
+ const currency = Amounts.currencyOf(contractData.amount);
+
const selectCoinsResult = await selectPayCoins(wex, {
restrictExchanges: {
auditors: [],
@@ -1819,24 +1878,35 @@ export async function confirmPay(
restrictWireMethod: contractData.wireMethod,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
- wireFeeAmortization: 1, // FIXME #8653
prevPayCoins: [],
requiredMinimumAge: contractData.minimumAge,
forcedSelection: forcedCoinSel,
});
- logger.trace("coin selection result", selectCoinsResult);
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
- if (selectCoinsResult.type === "failure") {
- // Should not happen, since checkPay should be called first
- // FIXME: Actually, this should be handled gracefully,
- // and the status should be stored in the DB.
- logger.warn("not confirming payment, insufficient coins");
- throw Error("insufficient balance");
+ switch (selectCoinsResult.type) {
+ case "failure": {
+ // Should not happen, since checkPay should be called first
+ // FIXME: Actually, this should be handled gracefully,
+ // and the status should be stored in the DB.
+ logger.warn("not confirming payment, insufficient coins");
+ throw Error("insufficient balance");
+ }
+ case "prospective": {
+ coins = selectCoinsResult.result.prospectiveCoins;
+ break;
+ }
+ case "success":
+ coins = selectCoinsResult.coinSel.coins;
+ break;
+ default:
+ assertUnreachable(selectCoinsResult);
}
- const coinSelection = selectCoinsResult.coinSel;
- const payCostInfo = await getTotalPaymentCost(wex, coinSelection);
+ logger.trace("coin selection result", selectCoinsResult);
+
+ const payCostInfo = await getTotalPaymentCost(wex, currency, coins);
let sessionId: string | undefined;
if (sessionIdOverride) {
@@ -1850,13 +1920,16 @@ export async function confirmPay(
);
const transitionInfo = await wex.db.runReadWriteTx(
- [
- "purchases",
- "coins",
- "refreshGroups",
- "denominations",
- "coinAvailability",
- ],
+ {
+ storeNames: [
+ "purchases",
+ "coins",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
async (tx) => {
const p = await tx.purchases.get(proposal.proposalId);
if (!p) {
@@ -1867,29 +1940,37 @@ export async function confirmPay(
case PurchaseStatus.DialogShared:
case PurchaseStatus.DialogProposed:
p.payInfo = {
- payCoinSelection: {
- coinContributions: coinSelection.coins.map((x) => x.contribution),
- coinPubs: coinSelection.coins.map((x) => x.coinPub),
- },
- payCoinSelectionUid: encodeCrock(getRandomBytes(16)),
totalPayCost: Amounts.stringify(payCostInfo),
};
+ if (selectCoinsResult.type === "success") {
+ p.payInfo.payCoinSelection = {
+ coinContributions: selectCoinsResult.coinSel.coins.map(
+ (x) => x.contribution,
+ ),
+ coinPubs: selectCoinsResult.coinSel.coins.map((x) => x.coinPub),
+ };
+ p.payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(16));
+ }
p.lastSessionId = sessionId;
p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now());
p.purchaseStatus = PurchaseStatus.PendingPaying;
await tx.purchases.put(p);
- await spendCoins(wex, tx, {
- //`txn:proposal:${p.proposalId}`
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.Payment,
- proposalId: proposalId,
- }),
- coinPubs: coinSelection.coins.map((x) => x.coinPub),
- contributions: coinSelection.coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- ),
- refreshReason: RefreshReason.PayMerchant,
- });
+ if (p.payInfo.payCoinSelection) {
+ const sel = p.payInfo.payCoinSelection;
+ await spendCoins(wex, tx, {
+ //`txn:proposal:${p.proposalId}`
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: proposalId,
+ }),
+ coinPubs: sel.coinPubs,
+ contributions: sel.coinContributions.map((x) =>
+ Amounts.parseOrThrow(x),
+ ),
+ refreshReason: RefreshReason.PayMerchant,
+ });
+ }
+
break;
case PurchaseStatus.Done:
case PurchaseStatus.PendingPaying:
@@ -1920,9 +2001,12 @@ export async function processPurchase(
wex: WalletExecutionContext,
proposalId: string,
): Promise<TaskRunResult> {
- const purchase = await wex.db.runReadOnlyTx(["purchases"], async (tx) => {
- return tx.purchases.get(proposalId);
- });
+ const purchase = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
if (!purchase) {
return {
type: TaskRunResultType.Error,
@@ -1979,9 +2063,12 @@ async function processPurchasePay(
wex: WalletExecutionContext,
proposalId: string,
): Promise<TaskRunResult> {
- const purchase = await wex.db.runReadOnlyTx(["purchases"], async (tx) => {
- return tx.purchases.get(proposalId);
- });
+ const purchase = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(proposalId);
+ },
+ );
if (!purchase) {
return {
type: TaskRunResultType.Error,
@@ -2003,6 +2090,8 @@ async function processPurchasePay(
}
logger.trace(`processing purchase pay ${proposalId}`);
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+
const sessionId = purchase.lastSessionId;
logger.trace(`paying with session ID ${sessionId}`);
@@ -2020,7 +2109,7 @@ async function processPurchasePay(
if (paid) {
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -2051,6 +2140,110 @@ async function processPurchasePay(
}
}
+ const contractData = download.contractData;
+ const currency = Amounts.currencyOf(download.contractData.amount);
+
+ if (!payInfo.payCoinSelection) {
+ const selectCoinsResult = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
+ contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+ depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+ prevPayCoins: [],
+ requiredMinimumAge: contractData.minimumAge,
+ });
+ switch (selectCoinsResult.type) {
+ case "failure": {
+ // Should not happen, since checkPay should be called first
+ // FIXME: Actually, this should be handled gracefully,
+ // and the status should be stored in the DB.
+ logger.warn("not confirming payment, insufficient coins");
+ throw Error("insufficient balance");
+ }
+ case "prospective": {
+ throw Error("insufficient balance (pending refresh)");
+ }
+ case "success":
+ break;
+ default:
+ assertUnreachable(selectCoinsResult);
+ }
+
+ logger.trace("coin selection result", selectCoinsResult);
+
+ const payCostInfo = await getTotalPaymentCost(
+ wex,
+ currency,
+ selectCoinsResult.coinSel.coins,
+ );
+
+ const transitionDone = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "purchases",
+ "coins",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const p = await tx.purchases.get(proposalId);
+ if (!p) {
+ return false;
+ }
+ if (p.payInfo?.payCoinSelection) {
+ return false;
+ }
+ switch (p.purchaseStatus) {
+ case PurchaseStatus.DialogShared:
+ case PurchaseStatus.DialogProposed:
+ p.payInfo = {
+ totalPayCost: Amounts.stringify(payCostInfo),
+ payCoinSelection: {
+ coinContributions: selectCoinsResult.coinSel.coins.map(
+ (x) => x.contribution,
+ ),
+ coinPubs: selectCoinsResult.coinSel.coins.map((x) => x.coinPub),
+ },
+ };
+ p.payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(16));
+ p.purchaseStatus = PurchaseStatus.PendingPaying;
+ await tx.purchases.put(p);
+
+ await spendCoins(wex, tx, {
+ //`txn:proposal:${p.proposalId}`
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: proposalId,
+ }),
+ coinPubs: selectCoinsResult.coinSel.coins.map((x) => x.coinPub),
+ contributions: selectCoinsResult.coinSel.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayMerchant,
+ });
+ return true;
+ case PurchaseStatus.Done:
+ case PurchaseStatus.PendingPaying:
+ default:
+ break;
+ }
+ return false;
+ },
+ );
+
+ if (transitionDone) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
+ }
+
if (!purchase.merchantPaySig) {
const payUrl = new URL(
`orders/${download.contractData.orderId}/pay`,
@@ -2105,6 +2298,7 @@ async function processPurchasePay(
TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS
) {
// Do this in the background, as it might take some time
+ // FIXME: Why? We're already in a (background) task!
handleInsufficientFunds(wex, proposalId, err).catch(async (e) => {
logger.error("handling insufficient funds failed");
logger.error(`${e.toString()}`);
@@ -2190,7 +2384,7 @@ export async function refuseProposal(
proposalId,
});
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const proposal = await tx.purchases.get(proposalId);
if (!proposal) {
@@ -2464,42 +2658,45 @@ export async function sharePayment(
merchantBaseUrl: string,
orderId: string,
): Promise<SharePaymentResult> {
- const result = await wex.db.runReadWriteTx(["purchases"], async (tx) => {
- const p = await tx.purchases.indexes.byUrlAndOrderId.get([
- merchantBaseUrl,
- orderId,
- ]);
- if (!p) {
- logger.warn("purchase does not exist anymore");
- return undefined;
- }
- if (
- p.purchaseStatus !== PurchaseStatus.DialogProposed &&
- p.purchaseStatus !== PurchaseStatus.DialogShared
- ) {
- // FIXME: purchase can be shared before being paid
- return undefined;
- }
- const oldTxState = computePayMerchantTransactionState(p);
- if (p.purchaseStatus === PurchaseStatus.DialogProposed) {
- p.purchaseStatus = PurchaseStatus.DialogShared;
- p.shared = true;
- await tx.purchases.put(p);
- }
+ const result = await wex.db.runReadWriteTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ const p = await tx.purchases.indexes.byUrlAndOrderId.get([
+ merchantBaseUrl,
+ orderId,
+ ]);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return undefined;
+ }
+ if (
+ p.purchaseStatus !== PurchaseStatus.DialogProposed &&
+ p.purchaseStatus !== PurchaseStatus.DialogShared
+ ) {
+ // FIXME: purchase can be shared before being paid
+ return undefined;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ if (p.purchaseStatus === PurchaseStatus.DialogProposed) {
+ p.purchaseStatus = PurchaseStatus.DialogShared;
+ p.shared = true;
+ await tx.purchases.put(p);
+ }
- const newTxState = computePayMerchantTransactionState(p);
+ const newTxState = computePayMerchantTransactionState(p);
- return {
- proposalId: p.proposalId,
- nonce: p.noncePriv,
- session: p.lastSessionId ?? p.downloadSessionId,
- token: p.claimToken,
- transitionInfo: {
- oldTxState,
- newTxState,
- },
- };
- });
+ return {
+ proposalId: p.proposalId,
+ nonce: p.noncePriv,
+ session: p.lastSessionId ?? p.downloadSessionId,
+ token: p.claimToken,
+ transitionInfo: {
+ oldTxState,
+ newTxState,
+ },
+ };
+ },
+ );
if (result === undefined) {
throw Error("This purchase can't be shared");
@@ -2574,7 +2771,7 @@ async function processPurchaseDialogShared(
);
if (paid) {
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -2613,23 +2810,47 @@ async function processPurchaseAutoRefund(
const download = await expectProposalDownload(wex, purchase);
- if (
+ const noAutoRefundOrExpired =
!purchase.autoRefundDeadline ||
AbsoluteTime.isExpired(
AbsoluteTime.fromProtocolTimestamp(
timestampProtocolFromDb(purchase.autoRefundDeadline),
),
- )
- ) {
+ );
+
+ const totalKnownRefund = await wex.db.runReadOnlyTx(
+ { storeNames: ["refundGroups"] },
+ async (tx) => {
+ const refunds = await tx.refundGroups.indexes.byProposalId.getAll(
+ purchase.proposalId,
+ );
+ const am = Amounts.parseOrThrow(download.contractData.amount);
+ return refunds.reduce((prev, cur) => {
+ if (
+ cur.status === RefundGroupStatus.Done ||
+ cur.status === RefundGroupStatus.Pending
+ ) {
+ return Amounts.add(prev, cur.amountEffective).amount;
+ }
+ return prev;
+ }, Amounts.zeroOfAmount(am));
+ },
+ );
+
+ const refundedIsLessThanPrice =
+ Amounts.cmp(download.contractData.amount, totalKnownRefund) === +1;
+ const nothingMoreToRefund = !refundedIsLessThanPrice;
+
+ if (noAutoRefundOrExpired || nothingMoreToRefund) {
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
logger.warn("purchase does not exist anymore");
return;
}
- if (p.purchaseStatus !== PurchaseStatus.PendingQueryingRefund) {
+ if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) {
return;
}
const oldTxState = computePayMerchantTransactionState(p);
@@ -2653,8 +2874,9 @@ async function processPurchaseAutoRefund(
download.contractData.contractTermsHash,
);
- requestUrl.searchParams.set("timeout_ms", "1000");
+ requestUrl.searchParams.set("timeout_ms", "10000");
requestUrl.searchParams.set("await_refund_obtained", "yes");
+ requestUrl.searchParams.set("refund", Amounts.stringify(totalKnownRefund));
const resp = await wex.http.fetch(requestUrl.href, {
cancellationToken: wex.cancellationToken,
@@ -2669,7 +2891,7 @@ async function processPurchaseAutoRefund(
if (orderStatus.refund_pending) {
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -2687,9 +2909,10 @@ async function processPurchaseAutoRefund(
},
);
notifyTransition(wex, transactionId, transitionInfo);
+ return TaskRunResult.progress();
}
- return TaskRunResult.backoff();
+ return TaskRunResult.longpollReturnedPending();
}
async function processPurchaseAbortingRefund(
@@ -2712,7 +2935,7 @@ async function processPurchaseAbortingRefund(
throw Error("can't abort, no coins selected");
}
- await wex.db.runReadOnlyTx(["coins"], async (tx) => {
+ await wex.db.runReadOnlyTx({ storeNames: ["coins"] }, async (tx) => {
for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
const coinPub = payCoinSelection.coinPubs[i];
const coin = await tx.coins.get(coinPub);
@@ -2818,7 +3041,7 @@ async function processPurchaseQueryRefund(
if (!orderStatus.refund_pending) {
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -2845,7 +3068,7 @@ async function processPurchaseQueryRefund(
).amount;
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const p = await tx.purchases.get(purchase.proposalId);
if (!p) {
@@ -2913,7 +3136,7 @@ export async function startRefundQueryForUri(
throw Error("expected taler://refund URI");
}
const purchaseRecord = await wex.db.runReadOnlyTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
return tx.purchases.indexes.byUrlAndOrderId.get([
parsedUri.merchantBaseUrl,
@@ -2944,7 +3167,7 @@ export async function startQueryRefund(
): Promise<void> {
const ctx = new PayMerchantTransactionContext(wex, proposalId);
const transitionInfo = await wex.db.runReadWriteTx(
- ["purchases"],
+ { storeNames: ["purchases"] },
async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
@@ -3037,17 +3260,20 @@ async function storeRefunds(
const currency = Amounts.currencyOf(download.contractData.amount);
const result = await wex.db.runReadWriteTx(
- [
- "coins",
- "denominations",
- "purchases",
- "refundItems",
- "refundGroups",
- "denominations",
- "coins",
- "coinAvailability",
- "refreshGroups",
- ],
+ {
+ storeNames: [
+ "coins",
+ "denominations",
+ "purchases",
+ "refundItems",
+ "refundGroups",
+ "denominations",
+ "coins",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ ],
+ },
async (tx) => {
const myPurchase = await tx.purchases.get(purchase.proposalId);
if (!myPurchase) {
@@ -3204,9 +3430,20 @@ async function storeRefunds(
}
const oldTxState = computePayMerchantTransactionState(myPurchase);
+
+ const shouldCheckAutoRefund =
+ myPurchase.autoRefundDeadline &&
+ !AbsoluteTime.isExpired(
+ AbsoluteTime.fromProtocolTimestamp(
+ timestampProtocolFromDb(myPurchase.autoRefundDeadline),
+ ),
+ );
+
if (numPendingItemsTotal === 0) {
if (isAborting) {
myPurchase.purchaseStatus = PurchaseStatus.AbortedRefunded;
+ } else if (shouldCheckAutoRefund) {
+ myPurchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund;
} else {
myPurchase.purchaseStatus = PurchaseStatus.Done;
}
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts
index 599010c1d..bfd39b657 100644
--- a/packages/taler-wallet-core/src/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/pay-peer-common.ts
@@ -22,7 +22,7 @@ import {
AmountString,
Amounts,
Codec,
- SelectedCoin,
+ SelectedProspectiveCoin,
TalerProtocolTimestamp,
buildCodecForObject,
checkDbInvariant,
@@ -34,7 +34,6 @@ import { SpendCoinDetails } from "./crypto/cryptoImplementation.js";
import { DbPeerPushPaymentCoinSelection, ReserveRecord } from "./db.js";
import { getTotalRefreshCost } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
-import { getCandidateWithdrawalDenomsTx } from "./withdraw.js";
/**
* Get information about the coin selected for signatures.
@@ -44,79 +43,74 @@ export async function queryCoinInfosForSelection(
csel: DbPeerPushPaymentCoinSelection,
): Promise<SpendCoinDetails[]> {
let infos: SpendCoinDetails[] = [];
- await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
- for (let i = 0; i < csel.coinPubs.length; i++) {
- const coin = await tx.coins.get(csel.coinPubs[i]);
- if (!coin) {
- throw Error("coin not found anymore");
- }
- const denom = await getDenomInfo(
- wex,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- if (!denom) {
- throw Error("denom for coin not found anymore");
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations"] },
+ async (tx) => {
+ for (let i = 0; i < csel.coinPubs.length; i++) {
+ const coin = await tx.coins.get(csel.coinPubs[i]);
+ if (!coin) {
+ throw Error("coin not found anymore");
+ }
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ if (!denom) {
+ throw Error("denom for coin not found anymore");
+ }
+ infos.push({
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ contribution: csel.contributions[i],
+ });
}
- infos.push({
- coinPriv: coin.coinPriv,
- coinPub: coin.coinPub,
- denomPubHash: coin.denomPubHash,
- denomSig: coin.denomSig,
- ageCommitmentProof: coin.ageCommitmentProof,
- contribution: csel.contributions[i],
- });
- }
- });
+ },
+ );
return infos;
}
export async function getTotalPeerPaymentCost(
wex: WalletExecutionContext,
- pcs: SelectedCoin[],
+ pcs: SelectedProspectiveCoin[],
): Promise<AmountJson> {
- const currency = Amounts.currencyOf(pcs[0].contribution);
- return wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
- const costs: AmountJson[] = [];
- for (let i = 0; i < pcs.length; i++) {
- const coin = await tx.coins.get(pcs[i].coinPub);
- if (!coin) {
- throw Error("can't calculate payment cost, coin not found");
- }
- const denomInfo = await getDenomInfo(
- wex,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- if (!denomInfo) {
- throw Error(
- "can't calculate payment cost, denomination for coin not found",
+ return wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations"] },
+ async (tx) => {
+ const costs: AmountJson[] = [];
+ for (let i = 0; i < pcs.length; i++) {
+ const denomInfo = await getDenomInfo(
+ wex,
+ tx,
+ pcs[i].exchangeBaseUrl,
+ pcs[i].denomPubHash,
+ );
+ if (!denomInfo) {
+ throw Error(
+ "can't calculate payment cost, denomination for coin not found",
+ );
+ }
+ const amountLeft = Amounts.sub(
+ denomInfo.value,
+ pcs[i].contribution,
+ ).amount;
+ const refreshCost = await getTotalRefreshCost(
+ wex,
+ tx,
+ denomInfo,
+ amountLeft,
);
+ costs.push(Amounts.parseOrThrow(pcs[i].contribution));
+ costs.push(refreshCost);
}
- const allDenoms = await getCandidateWithdrawalDenomsTx(
- wex,
- tx,
- coin.exchangeBaseUrl,
- currency,
- );
- const amountLeft = Amounts.sub(
- denomInfo.value,
- pcs[i].contribution,
- ).amount;
- const refreshCost = getTotalRefreshCost(
- allDenoms,
- denomInfo,
- amountLeft,
- wex.ws.config.testing.denomselAllowLate,
- );
- costs.push(Amounts.parseOrThrow(pcs[i].contribution));
- costs.push(refreshCost);
- }
- const zero = Amounts.zeroOfAmount(pcs[0].contribution);
- return Amounts.sum([zero, ...costs]).amount;
- });
+ const zero = Amounts.zeroOfAmount(pcs[0].contribution);
+ return Amounts.sum([zero, ...costs]).amount;
+ },
+ );
}
interface ExchangePurseStatus {
@@ -143,7 +137,7 @@ export async function getMergeReserveInfo(
const newReservePair = await wex.cryptoApi.createEddsaKeypair({});
const mergeReserveRecord: ReserveRecord = await wex.db.runReadWriteTx(
- ["exchanges", "reserves"],
+ { storeNames: ["exchanges", "reserves"] },
async (tx) => {
const ex = await tx.exchanges.get(req.exchangeBaseUrl);
checkDbInvariant(!!ex);
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
index 4155f83e6..840c244d0 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -110,7 +110,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
async deleteTransaction(): Promise<void> {
const { wex: ws, pursePub } = this;
await ws.db.runReadWriteTx(
- ["withdrawalGroups", "peerPullCredit", "tombstones"],
+ { storeNames: ["withdrawalGroups", "peerPullCredit", "tombstones"] },
async (tx) => {
const pullIni = await tx.peerPullCredit.get(pursePub);
if (!pullIni) {
@@ -140,7 +140,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
async suspendTransaction(): Promise<void> {
const { wex, pursePub, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullCredit"],
+ { storeNames: ["peerPullCredit"] },
async (tx) => {
const pullCreditRec = await tx.peerPullCredit.get(pursePub);
if (!pullCreditRec) {
@@ -200,7 +200,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
async failTransaction(): Promise<void> {
const { wex, pursePub, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullCredit"],
+ { storeNames: ["peerPullCredit"] },
async (tx) => {
const pullCreditRec = await tx.peerPullCredit.get(pursePub);
if (!pullCreditRec) {
@@ -251,7 +251,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
async resumeTransaction(): Promise<void> {
const { wex, pursePub, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullCredit"],
+ { storeNames: ["peerPullCredit"] },
async (tx) => {
const pullCreditRec = await tx.peerPullCredit.get(pursePub);
if (!pullCreditRec) {
@@ -310,7 +310,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
async abortTransaction(): Promise<void> {
const { wex, pursePub, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullCredit"],
+ { storeNames: ["peerPullCredit"] },
async (tx) => {
const pullCreditRec = await tx.peerPullCredit.get(pursePub);
if (!pullCreditRec) {
@@ -388,7 +388,7 @@ async function queryPurseForPeerPullCredit(
case HttpStatusCode.Gone: {
// Exchange says that purse doesn't exist anymore => expired!
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullCredit"],
+ { storeNames: ["peerPullCredit"] },
async (tx) => {
const finPi = await tx.peerPullCredit.get(pullIni.pursePub);
if (!finPi) {
@@ -426,9 +426,12 @@ async function queryPurseForPeerPullCredit(
return TaskRunResult.backoff();
}
- const reserve = await wex.db.runReadOnlyTx(["reserves"], async (tx) => {
- return await tx.reserves.get(pullIni.mergeReserveRowId);
- });
+ const reserve = await wex.db.runReadOnlyTx(
+ { storeNames: ["reserves"] },
+ async (tx) => {
+ return await tx.reserves.get(pullIni.mergeReserveRowId);
+ },
+ );
if (!reserve) {
throw Error("reserve for peer pull credit not found in wallet DB");
@@ -449,7 +452,7 @@ async function queryPurseForPeerPullCredit(
},
});
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullCredit"],
+ { storeNames: ["peerPullCredit"] },
async (tx) => {
const finPi = await tx.peerPullCredit.get(pullIni.pursePub);
if (!finPi) {
@@ -497,7 +500,7 @@ async function longpollKycStatus(
kycStatusRes.status === HttpStatusCode.NoContent
) {
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullCredit"],
+ { storeNames: ["peerPullCredit"] },
async (tx) => {
const peerIni = await tx.peerPullCredit.get(pursePub);
if (!peerIni) {
@@ -548,13 +551,15 @@ async function processPeerPullCreditAbortingDeletePurse(
logger.info(`deleted purse with response status ${resp.status}`);
const transitionInfo = await wex.db.runReadWriteTx(
- [
- "peerPullCredit",
- "refreshGroups",
- "denominations",
- "coinAvailability",
- "coins",
- ],
+ {
+ storeNames: [
+ "peerPullCredit",
+ "refreshGroups",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ ],
+ },
async (tx) => {
const ppiRec = await tx.peerPullCredit.get(pursePub);
if (!ppiRec) {
@@ -593,7 +598,7 @@ async function handlePeerPullCreditWithdrawing(
const wgId = pullIni.withdrawalGroupId;
let finished: boolean = false;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullCredit", "withdrawalGroups"],
+ { storeNames: ["peerPullCredit", "withdrawalGroups"] },
async (tx) => {
const ppi = await tx.peerPullCredit.get(pullIni.pursePub);
if (!ppi) {
@@ -640,16 +645,19 @@ async function handlePeerPullCreditCreatePurse(
): Promise<TaskRunResult> {
const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount));
const pursePub = pullIni.pursePub;
- const mergeReserve = await wex.db.runReadOnlyTx(["reserves"], async (tx) => {
- return tx.reserves.get(pullIni.mergeReserveRowId);
- });
+ const mergeReserve = await wex.db.runReadOnlyTx(
+ { storeNames: ["reserves"] },
+ async (tx) => {
+ return tx.reserves.get(pullIni.mergeReserveRowId);
+ },
+ );
if (!mergeReserve) {
throw Error("merge reserve for peer pull payment not found in database");
}
const contractTermsRecord = await wex.db.runReadOnlyTx(
- ["contractTerms"],
+ { storeNames: ["contractTerms"] },
async (tx) => {
return tx.contractTerms.get(pullIni.contractTermsHash);
},
@@ -737,7 +745,7 @@ async function handlePeerPullCreditCreatePurse(
});
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullCredit"],
+ { storeNames: ["peerPullCredit"] },
async (tx) => {
const pi2 = await tx.peerPullCredit.get(pursePub);
if (!pi2) {
@@ -758,9 +766,12 @@ export async function processPeerPullCredit(
wex: WalletExecutionContext,
pursePub: string,
): Promise<TaskRunResult> {
- const pullIni = await wex.db.runReadOnlyTx(["peerPullCredit"], async (tx) => {
- return tx.peerPullCredit.get(pursePub);
- });
+ const pullIni = await wex.db.runReadOnlyTx(
+ { storeNames: ["peerPullCredit"] },
+ async (tx) => {
+ return tx.peerPullCredit.get(pursePub);
+ },
+ );
if (!pullIni) {
throw Error("peer pull payment initiation not found in database");
}
@@ -847,7 +858,7 @@ async function processPeerPullCreditKycRequired(
const kycStatus = await kycStatusRes.json();
logger.info(`kyc status: ${j2s(kycStatus)}`);
const { transitionInfo, result } = await wex.db.runReadWriteTx(
- ["peerPullCredit"],
+ { storeNames: ["peerPullCredit"] },
async (tx) => {
const peerInc = await tx.peerPullCredit.get(pursePub);
if (!peerInc) {
@@ -947,42 +958,45 @@ async function getPreferredExchangeForCurrency(
): Promise<string | undefined> {
// Find an exchange with the matching currency.
// Prefer exchanges with the most recent withdrawal.
- const url = await wex.db.runReadOnlyTx(["exchanges"], async (tx) => {
- const exchanges = await tx.exchanges.iter().toArray();
- let candidate = undefined;
- for (const e of exchanges) {
- if (e.detailsPointer?.currency !== currency) {
- continue;
- }
- if (!candidate) {
- candidate = e;
- continue;
- }
- if (candidate.lastWithdrawal && !e.lastWithdrawal) {
- continue;
- }
- const exchangeLastWithdrawal = timestampOptionalPreciseFromDb(
- e.lastWithdrawal,
- );
- const candidateLastWithdrawal = timestampOptionalPreciseFromDb(
- candidate.lastWithdrawal,
- );
- if (exchangeLastWithdrawal && candidateLastWithdrawal) {
- if (
- AbsoluteTime.cmp(
- AbsoluteTime.fromPreciseTimestamp(exchangeLastWithdrawal),
- AbsoluteTime.fromPreciseTimestamp(candidateLastWithdrawal),
- ) > 0
- ) {
+ const url = await wex.db.runReadOnlyTx(
+ { storeNames: ["exchanges"] },
+ async (tx) => {
+ const exchanges = await tx.exchanges.iter().toArray();
+ let candidate = undefined;
+ for (const e of exchanges) {
+ if (e.detailsPointer?.currency !== currency) {
+ continue;
+ }
+ if (!candidate) {
candidate = e;
+ continue;
+ }
+ if (candidate.lastWithdrawal && !e.lastWithdrawal) {
+ continue;
+ }
+ const exchangeLastWithdrawal = timestampOptionalPreciseFromDb(
+ e.lastWithdrawal,
+ );
+ const candidateLastWithdrawal = timestampOptionalPreciseFromDb(
+ candidate.lastWithdrawal,
+ );
+ if (exchangeLastWithdrawal && candidateLastWithdrawal) {
+ if (
+ AbsoluteTime.cmp(
+ AbsoluteTime.fromPreciseTimestamp(exchangeLastWithdrawal),
+ AbsoluteTime.fromPreciseTimestamp(candidateLastWithdrawal),
+ ) > 0
+ ) {
+ candidate = e;
+ }
}
}
- }
- if (candidate) {
- return candidate.baseUrl;
- }
- return undefined;
- });
+ if (candidate) {
+ return candidate.baseUrl;
+ }
+ return undefined;
+ },
+ );
return url;
}
@@ -1039,7 +1053,7 @@ export async function initiatePeerPullPayment(
const mergeTimestamp = TalerPreciseTimestamp.now();
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullCredit", "contractTerms"],
+ { storeNames: ["peerPullCredit", "contractTerms"] },
async (tx) => {
const ppi: PeerPullCreditRecord = {
amount: req.partialContractTerms.amount,
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
index 6cc552714..0355b58ad 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -33,10 +33,12 @@ import {
HttpStatusCode,
Logger,
NotificationType,
+ ObservabilityEventType,
PeerContractTerms,
PreparePeerPullDebitRequest,
PreparePeerPullDebitResponse,
RefreshReason,
+ SelectedProspectiveCoin,
TalerError,
TalerErrorCode,
TalerPreciseTimestamp,
@@ -124,13 +126,16 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
const transactionId = this.transactionId;
const ws = this.wex;
const peerPullDebitId = this.peerPullDebitId;
- await ws.db.runReadWriteTx(["peerPullDebit", "tombstones"], async (tx) => {
- const debit = await tx.peerPullDebit.get(peerPullDebitId);
- if (debit) {
- await tx.peerPullDebit.delete(peerPullDebitId);
- await tx.tombstones.put({ id: transactionId });
- }
- });
+ await ws.db.runReadWriteTx(
+ { storeNames: ["peerPullDebit", "tombstones"] },
+ async (tx) => {
+ const debit = await tx.peerPullDebit.get(peerPullDebitId);
+ if (debit) {
+ await tx.peerPullDebit.delete(peerPullDebitId);
+ await tx.tombstones.put({ id: transactionId });
+ }
+ },
+ );
}
async suspendTransaction(): Promise<void> {
@@ -139,7 +144,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
const wex = this.wex;
const peerPullDebitId = this.peerPullDebitId;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullDebit"],
+ { storeNames: ["peerPullDebit"] },
async (tx) => {
const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
if (!pullDebitRec) {
@@ -234,6 +239,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
"coinAvailability",
"denominations",
"refreshGroups",
+ "refreshSessions",
"coins",
"coinAvailability",
],
@@ -302,7 +308,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
const wex = this.wex;
const extraStores = opts.extraStores ?? [];
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullDebit", ...extraStores],
+ { storeNames: ["peerPullDebit", ...extraStores] },
async (tx) => {
const pi = await tx.peerPullDebit.get(this.peerPullDebitId);
if (!pi) {
@@ -369,13 +375,25 @@ async function handlePurseCreationConflict(
}
}
- const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair });
+ const coinSelRes = await selectPeerCoins(ws, {
+ instructedAmount,
+ repair,
+ });
- if (coinSelRes.type == "failure") {
- // FIXME: Details!
- throw Error(
- "insufficient balance to re-select coins to repair double spending",
- );
+ switch (coinSelRes.type) {
+ case "failure":
+ // FIXME: Details!
+ throw Error(
+ "insufficient balance to re-select coins to repair double spending",
+ );
+ case "prospective":
+ throw Error(
+ "insufficient balance to re-select coins to repair double spending (blocked on refresh)",
+ );
+ case "success":
+ break;
+ default:
+ assertUnreachable(coinSelRes);
}
const totalAmount = await getTotalPeerPaymentCost(
@@ -383,7 +401,7 @@ async function handlePurseCreationConflict(
coinSelRes.result.coins,
);
- await ws.db.runReadWriteTx(["peerPullDebit"], async (tx) => {
+ await ws.db.runReadWriteTx({ storeNames: ["peerPullDebit"] }, async (tx) => {
const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
if (!myPpi) {
return;
@@ -411,77 +429,176 @@ async function processPeerPullDebitPendingDeposit(
wex: WalletExecutionContext,
peerPullInc: PeerPullPaymentIncomingRecord,
): Promise<TaskRunResult> {
+ const ctx = new PeerPullDebitTransactionContext(
+ wex,
+ peerPullInc.peerPullDebitId,
+ );
+
const pursePub = peerPullInc.pursePub;
const coinSel = peerPullInc.coinSel;
+
if (!coinSel) {
- throw Error("invalid state, no coins selected");
- }
+ const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
- const coins = await queryCoinInfosForSelection(wex, coinSel);
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ });
+ if (logger.shouldLogTrace()) {
+ logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
+ }
- const depositSigsResp = await wex.cryptoApi.signPurseDeposits({
- exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
- pursePub: peerPullInc.pursePub,
- coins,
- });
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ throw Error("insufficient balance (locked behind refresh)");
+ case "success":
+ coins = coinSelRes.result.coins;
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+
+ const peerPullDebitId = peerPullInc.peerPullDebitId;
+ const totalAmount = await getTotalPeerPaymentCost(wex, coins);
+
+ // FIXME: Missing notification here!
+
+ const transitionDone = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "exchanges",
+ "coins",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "peerPullDebit",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const pi = await tx.peerPullDebit.get(peerPullDebitId);
+ if (!pi) {
+ return false;
+ }
+ if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+ return false;
+ }
+ if (pi.coinSel) {
+ return false;
+ }
+ await spendCoins(wex, tx, {
+ // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId,
+ }),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPull,
+ });
+ pi.coinSel = {
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) => x.contribution),
+ totalCost: Amounts.stringify(totalAmount),
+ };
+ await tx.peerPullDebit.put(pi);
+ return true;
+ },
+ );
+ if (transitionDone) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
+ }
const purseDepositUrl = new URL(
`purses/${pursePub}/deposit`,
peerPullInc.exchangeBaseUrl,
);
- const depositPayload: ExchangePurseDeposits = {
- deposits: depositSigsResp.deposits,
- };
+ // FIXME: We could skip batches that we've already submitted.
- if (logger.shouldLogTrace()) {
- logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
- }
+ const coins = await queryCoinInfosForSelection(wex, coinSel);
- const httpResp = await wex.http.fetch(purseDepositUrl.href, {
- method: "POST",
- body: depositPayload,
- cancellationToken: wex.cancellationToken,
- });
+ const maxBatchSize = 100;
- const ctx = new PeerPullDebitTransactionContext(
- wex,
- peerPullInc.peerPullDebitId,
- );
+ for (let i = 0; i < coins.length; i += maxBatchSize) {
+ const batchSize = Math.min(maxBatchSize, coins.length - i);
- switch (httpResp.status) {
- case HttpStatusCode.Ok: {
- const resp = await readSuccessResponseJsonOrThrow(
- httpResp,
- codecForAny(),
- );
- logger.trace(`purse deposit response: ${j2s(resp)}`);
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Depositing batch at ${i}/${coins.length} of size ${batchSize}`,
+ });
- await ctx.transition(async (r) => {
- if (r.status !== PeerPullDebitRecordStatus.PendingDeposit) {
- return TransitionResultType.Stay;
- }
- r.status = PeerPullDebitRecordStatus.Done;
- return TransitionResultType.Transition;
- });
- return TaskRunResult.finished();
- }
- case HttpStatusCode.Gone: {
- await ctx.abortTransaction();
- return TaskRunResult.backoff();
- }
- case HttpStatusCode.Conflict: {
- return handlePurseCreationConflict(ctx, peerPullInc, httpResp);
+ const batchCoins = coins.slice(i, i + batchSize);
+ const depositSigsResp = await wex.cryptoApi.signPurseDeposits({
+ exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
+ pursePub: peerPullInc.pursePub,
+ coins: batchCoins,
+ });
+
+ const depositPayload: ExchangePurseDeposits = {
+ deposits: depositSigsResp.deposits,
+ };
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
}
- default: {
- const errResp = await readTalerErrorResponse(httpResp);
- return {
- type: TaskRunResultType.Error,
- errorDetail: errResp,
- };
+
+ const httpResp = await wex.http.fetch(purseDepositUrl.href, {
+ method: "POST",
+ body: depositPayload,
+ cancellationToken: wex.cancellationToken,
+ });
+
+ switch (httpResp.status) {
+ case HttpStatusCode.Ok: {
+ const resp = await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForAny(),
+ );
+ logger.trace(`purse deposit response: ${j2s(resp)}`);
+ continue;
+ }
+ case HttpStatusCode.Gone: {
+ await ctx.abortTransaction();
+ return TaskRunResult.backoff();
+ }
+ case HttpStatusCode.Conflict: {
+ return handlePurseCreationConflict(ctx, peerPullInc, httpResp);
+ }
+ default: {
+ const errResp = await readTalerErrorResponse(httpResp);
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: errResp,
+ };
+ }
}
}
+
+ // All batches succeeded, we can transition!
+
+ await ctx.transition(async (r) => {
+ if (r.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+ return TransitionResultType.Stay;
+ }
+ r.status = PeerPullDebitRecordStatus.Done;
+ return TransitionResultType.Transition;
+ });
+ return TaskRunResult.finished();
}
async function processPeerPullDebitAbortingRefresh(
@@ -496,7 +613,7 @@ async function processPeerPullDebitAbortingRefresh(
peerPullDebitId,
});
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPullDebit", "refreshGroups"],
+ { storeNames: ["peerPullDebit", "refreshGroups"] },
async (tx) => {
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
let newOpState: PeerPullDebitRecordStatus | undefined;
@@ -538,7 +655,7 @@ export async function processPeerPullDebit(
peerPullDebitId: string,
): Promise<TaskRunResult> {
const peerPullInc = await wex.db.runReadOnlyTx(
- ["peerPullDebit"],
+ { storeNames: ["peerPullDebit"] },
async (tx) => {
return tx.peerPullDebit.get(peerPullDebitId);
},
@@ -568,7 +685,7 @@ export async function confirmPeerPullDebit(
peerPullDebitId = parsedTx.peerPullDebitId;
const peerPullInc = await wex.db.runReadOnlyTx(
- ["peerPullDebit"],
+ { storeNames: ["peerPullDebit"] },
async (tx) => {
return tx.peerPullDebit.get(peerPullDebitId);
},
@@ -582,62 +699,77 @@ export async function confirmPeerPullDebit(
const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
- const coinSelRes = await selectPeerCoins(wex, { instructedAmount });
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ });
if (logger.shouldLogTrace()) {
logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
}
- if (coinSelRes.type !== "success") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
- },
- );
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = coinSelRes.result.prospectiveCoins;
+ break;
+ case "success":
+ coins = coinSelRes.result.coins;
+ break;
+ default:
+ assertUnreachable(coinSelRes);
}
- const sel = coinSelRes.result;
+ const totalAmount = await getTotalPeerPaymentCost(wex, coins);
- const totalAmount = await getTotalPeerPaymentCost(
- wex,
- coinSelRes.result.coins,
- );
+ // FIXME: Missing notification here!
await wex.db.runReadWriteTx(
- [
- "exchanges",
- "coins",
- "denominations",
- "refreshGroups",
- "peerPullDebit",
- "coinAvailability",
- ],
+ {
+ storeNames: [
+ "exchanges",
+ "coins",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "peerPullDebit",
+ "coinAvailability",
+ ],
+ },
async (tx) => {
- await spendCoins(wex, tx, {
- // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.PeerPullDebit,
- peerPullDebitId,
- }),
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- ),
- refreshReason: RefreshReason.PayPeerPull,
- });
-
const pi = await tx.peerPullDebit.get(peerPullDebitId);
if (!pi) {
throw Error();
}
- if (pi.status === PeerPullDebitRecordStatus.DialogProposed) {
- pi.status = PeerPullDebitRecordStatus.PendingDeposit;
+ if (pi.status !== PeerPullDebitRecordStatus.DialogProposed) {
+ return;
+ }
+ if (coinSelRes.type == "success") {
+ await spendCoins(wex, tx, {
+ // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPullDebit,
+ peerPullDebitId,
+ }),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPull,
+ });
pi.coinSel = {
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) => x.contribution),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) => x.contribution),
totalCost: Amounts.stringify(totalAmount),
};
}
+ pi.status = PeerPullDebitRecordStatus.PendingDeposit;
await tx.peerPullDebit.put(pi);
},
);
@@ -673,7 +805,7 @@ export async function preparePeerPullDebit(
}
const existing = await wex.db.runReadOnlyTx(
- ["peerPullDebit", "contractTerms"],
+ { storeNames: ["peerPullDebit", "contractTerms"] },
async (tx) => {
const peerPullDebitRecord =
await tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([
@@ -756,27 +888,37 @@ export async function preparePeerPullDebit(
const instructedAmount = Amounts.parseOrThrow(contractTerms.amount);
- const coinSelRes = await selectPeerCoins(wex, { instructedAmount });
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ });
if (logger.shouldLogTrace()) {
logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
}
- if (coinSelRes.type !== "success") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
- },
- );
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = coinSelRes.result.prospectiveCoins;
+ break;
+ case "success":
+ coins = coinSelRes.result.coins;
+ break;
+ default:
+ assertUnreachable(coinSelRes);
}
- const totalAmount = await getTotalPeerPaymentCost(
- wex,
- coinSelRes.result.coins,
- );
+ const totalAmount = await getTotalPeerPaymentCost(wex, coins);
await wex.db.runReadWriteTx(
- ["peerPullDebit", "contractTerms"],
+ { storeNames: ["peerPullDebit", "contractTerms"] },
async (tx) => {
await tx.contractTerms.put({
h: contractTermsHash,
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
index 281b3ff61..93f1a63a7 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -114,7 +114,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
async deleteTransaction(): Promise<void> {
const { wex, peerPushCreditId } = this;
await wex.db.runReadWriteTx(
- ["withdrawalGroups", "peerPushCredit", "tombstones"],
+ { storeNames: ["withdrawalGroups", "peerPushCredit", "tombstones"] },
async (tx) => {
const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushInc) {
@@ -143,7 +143,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
async suspendTransaction(): Promise<void> {
const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushCredit"],
+ { storeNames: ["peerPushCredit"] },
async (tx) => {
const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
@@ -197,7 +197,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
async abortTransaction(): Promise<void> {
const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushCredit"],
+ { storeNames: ["peerPushCredit"] },
async (tx) => {
const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
@@ -254,7 +254,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
async resumeTransaction(): Promise<void> {
const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushCredit"],
+ { storeNames: ["peerPushCredit"] },
async (tx) => {
const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
@@ -307,7 +307,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
async failTransaction(): Promise<void> {
const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushCredit"],
+ { storeNames: ["peerPushCredit"] },
async (tx) => {
const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushCreditRec) {
@@ -365,7 +365,7 @@ export async function preparePeerPushCredit(
}
const existing = await wex.db.runReadOnlyTx(
- ["contractTerms", "peerPushCredit"],
+ { storeNames: ["contractTerms", "peerPushCredit"] },
async (tx) => {
const existingPushInc =
await tx.peerPushCredit.indexes.byExchangeAndContractPriv.get([
@@ -460,7 +460,7 @@ export async function preparePeerPushCredit(
);
const transitionInfo = await wex.db.runReadWriteTx(
- ["contractTerms", "peerPushCredit"],
+ { storeNames: ["contractTerms", "peerPushCredit"] },
async (tx) => {
const rec: PeerPushPaymentIncomingRecord = {
peerPushCreditId,
@@ -545,7 +545,7 @@ async function longpollKycStatus(
kycStatusRes.status === HttpStatusCode.NoContent
) {
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushCredit"],
+ { storeNames: ["peerPushCredit"] },
async (tx) => {
const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
@@ -606,7 +606,7 @@ async function processPeerPushCreditKycRequired(
const kycStatus = await kycStatusRes.json();
logger.info(`kyc status: ${j2s(kycStatus)}`);
const { transitionInfo, result } = await wex.db.runReadWriteTx(
- ["peerPushCredit"],
+ { storeNames: ["peerPushCredit"] },
async (tx) => {
const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
@@ -731,14 +731,16 @@ async function handlePendingMerge(
});
const txRes = await wex.db.runReadWriteTx(
- [
- "contractTerms",
- "peerPushCredit",
- "withdrawalGroups",
- "reserves",
- "exchanges",
- "exchangeDetails",
- ],
+ {
+ storeNames: [
+ "contractTerms",
+ "peerPushCredit",
+ "withdrawalGroups",
+ "reserves",
+ "exchanges",
+ "exchangeDetails",
+ ],
+ },
async (tx) => {
const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
@@ -798,7 +800,7 @@ async function handlePendingWithdrawing(
const wgId = peerInc.withdrawalGroupId;
let finished: boolean = false;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushCredit", "withdrawalGroups"],
+ { storeNames: ["peerPushCredit", "withdrawalGroups"] },
async (tx) => {
const ppi = await tx.peerPushCredit.get(peerInc.peerPushCreditId);
if (!ppi) {
@@ -846,7 +848,7 @@ export async function processPeerPushCredit(
let peerInc: PeerPushPaymentIncomingRecord | undefined;
let contractTerms: PeerContractTerms | undefined;
await wex.db.runReadWriteTx(
- ["contractTerms", "peerPushCredit"],
+ { storeNames: ["contractTerms", "peerPushCredit"] },
async (tx) => {
peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
@@ -915,7 +917,7 @@ export async function confirmPeerPushCredit(
logger.trace(`confirming peer-push-credit ${peerPushCreditId}`);
await wex.db.runReadWriteTx(
- ["contractTerms", "peerPushCredit"],
+ { storeNames: ["contractTerms", "peerPushCredit"] },
async (tx) => {
peerInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!peerInc) {
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
index ab80888eb..6452407ff 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -20,12 +20,14 @@ import {
CheckPeerPushDebitResponse,
CoinRefreshRequest,
ContractTermsUtil,
+ ExchangePurseDeposits,
HttpStatusCode,
InitiatePeerPushDebitRequest,
InitiatePeerPushDebitResponse,
Logger,
NotificationType,
RefreshReason,
+ SelectedProspectiveCoin,
TalerError,
TalerErrorCode,
TalerPreciseTimestamp,
@@ -38,6 +40,7 @@ import {
TransactionState,
TransactionType,
assertUnreachable,
+ checkDbInvariant,
checkLogicInvariant,
encodeCrock,
getRandomBytes,
@@ -101,19 +104,22 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
async deleteTransaction(): Promise<void> {
const { wex, pursePub, transactionId } = this;
- await wex.db.runReadWriteTx(["peerPushDebit", "tombstones"], async (tx) => {
- const debit = await tx.peerPushDebit.get(pursePub);
- if (debit) {
- await tx.peerPushDebit.delete(pursePub);
- await tx.tombstones.put({ id: transactionId });
- }
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["peerPushDebit", "tombstones"] },
+ async (tx) => {
+ const debit = await tx.peerPushDebit.get(pursePub);
+ if (debit) {
+ await tx.peerPushDebit.delete(pursePub);
+ await tx.tombstones.put({ id: transactionId });
+ }
+ },
+ );
}
async suspendTransaction(): Promise<void> {
const { wex, pursePub, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushDebit"],
+ { storeNames: ["peerPushDebit"] },
async (tx) => {
const pushDebitRec = await tx.peerPushDebit.get(pursePub);
if (!pushDebitRec) {
@@ -171,7 +177,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
async abortTransaction(): Promise<void> {
const { wex, pursePub, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushDebit"],
+ { storeNames: ["peerPushDebit"] },
async (tx) => {
const pushDebitRec = await tx.peerPushDebit.get(pursePub);
if (!pushDebitRec) {
@@ -225,7 +231,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
async resumeTransaction(): Promise<void> {
const { wex, pursePub, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushDebit"],
+ { storeNames: ["peerPushDebit"] },
async (tx) => {
const pushDebitRec = await tx.peerPushDebit.get(pursePub);
if (!pushDebitRec) {
@@ -283,7 +289,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
async failTransaction(): Promise<void> {
const { wex, pursePub, transactionId, taskId: retryTag } = this;
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushDebit"],
+ { storeNames: ["peerPushDebit"] },
async (tx) => {
const pushDebitRec = await tx.peerPushDebit.get(pursePub);
if (!pushDebitRec) {
@@ -343,20 +349,29 @@ export async function checkPeerPushDebit(
logger.trace(
`checking peer push debit for ${Amounts.stringify(instructedAmount)}`,
);
- const coinSelRes = await selectPeerCoins(wex, { instructedAmount });
- if (coinSelRes.type === "failure") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
- },
- );
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ });
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = coinSelRes.result.prospectiveCoins;
+ break;
+ case "success":
+ coins = coinSelRes.result.coins;
+ break;
+ default:
+ assertUnreachable(coinSelRes);
}
- logger.trace(`selected peer coins (len=${coinSelRes.result.coins.length})`);
- const totalAmount = await getTotalPeerPaymentCost(
- wex,
- coinSelRes.result.coins,
- );
+ logger.trace(`selected peer coins (len=${coins.length})`);
+ const totalAmount = await getTotalPeerPaymentCost(wex, coins);
logger.trace("computed total peer payment cost");
return {
exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
@@ -391,6 +406,8 @@ async function handlePurseCreationConflict(
const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
const sel = peerPushInitiation.coinSel;
+ checkDbInvariant(!!sel);
+
const repair: PreviousPayCoins = [];
for (let i = 0; i < sel.coinPubs.length; i++) {
@@ -402,16 +419,25 @@ async function handlePurseCreationConflict(
}
}
- const coinSelRes = await selectPeerCoins(wex, { instructedAmount, repair });
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ repair,
+ });
- if (coinSelRes.type == "failure") {
- // FIXME: Details!
- throw Error(
- "insufficient balance to re-select coins to repair double spending",
- );
+ switch (coinSelRes.type) {
+ case "failure":
+ case "prospective":
+ // FIXME: Details!
+ throw Error(
+ "insufficient balance to re-select coins to repair double spending",
+ );
+ case "success":
+ break;
+ default:
+ assertUnreachable(coinSelRes);
}
- await wex.db.runReadWriteTx(["peerPushDebit"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["peerPushDebit"] }, async (tx) => {
const myPpi = await tx.peerPushDebit.get(peerPushInitiation.pursePub);
if (!myPpi) {
return;
@@ -447,7 +473,7 @@ async function processPeerPushDebitCreateReserve(
logger.trace(`processing ${transactionId} pending(create-reserve)`);
const contractTermsRecord = await wex.db.runReadOnlyTx(
- ["contractTerms"],
+ { storeNames: ["contractTerms"] },
async (tx) => {
return tx.contractTerms.get(hContractTerms);
},
@@ -459,6 +485,77 @@ async function processPeerPushDebitCreateReserve(
);
}
+ if (!peerPushInitiation.coinSel) {
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount: Amounts.parseOrThrow(peerPushInitiation.amount),
+ });
+
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ throw Error("insufficient funds (blocked on refresh)");
+ case "success":
+ break;
+ default:
+ assertUnreachable(coinSelRes);
+ }
+ const transitionDone = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "peerPushDebit",
+ ],
+ },
+ async (tx) => {
+ const ppi = await tx.peerPushDebit.get(pursePub);
+ if (!ppi) {
+ return false;
+ }
+ if (ppi.coinSel) {
+ return false;
+ }
+
+ ppi.coinSel = {
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) => x.contribution),
+ };
+ // FIXME: Instead of directly doing a spendCoin here,
+ // we might want to mark the coins as used and spend them
+ // after we've been able to create the purse.
+ await spendCoins(wex, tx, {
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub,
+ }),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPush,
+ });
+
+ await tx.peerPushDebit.put(ppi);
+ return true;
+ },
+ );
+ if (transitionDone) {
+ return TaskRunResult.progress();
+ }
+ return TaskRunResult.backoff();
+ }
+
const purseSigResp = await wex.cryptoApi.signPurseCreation({
hContractTerms,
mergePub: peerPushInitiation.mergePub,
@@ -473,12 +570,6 @@ async function processPeerPushDebitCreateReserve(
peerPushInitiation.coinSel,
);
- const depositSigsResp = await wex.cryptoApi.signPurseDeposits({
- exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl,
- pursePub: peerPushInitiation.pursePub,
- coins,
- });
-
const encryptContractRequest: EncryptContractRequest = {
contractTerms: contractTermsRecord.contractTermsRaw,
mergePriv: peerPushInitiation.mergePriv,
@@ -489,66 +580,115 @@ async function processPeerPushDebitCreateReserve(
nonce: peerPushInitiation.contractEncNonce,
};
- logger.trace(`encrypt contract request: ${j2s(encryptContractRequest)}`);
-
const econtractResp = await wex.cryptoApi.encryptContractForMerge(
encryptContractRequest,
);
- const createPurseUrl = new URL(
- `purses/${peerPushInitiation.pursePub}/create`,
- peerPushInitiation.exchangeBaseUrl,
- );
+ const maxBatchSize = 100;
- const reqBody = {
- amount: peerPushInitiation.amount,
- merge_pub: peerPushInitiation.mergePub,
- purse_sig: purseSigResp.sig,
- h_contract_terms: hContractTerms,
- purse_expiration: timestampProtocolFromDb(purseExpiration),
- deposits: depositSigsResp.deposits,
- min_age: 0,
- econtract: econtractResp.econtract,
- };
+ for (let i = 0; i < coins.length; i += maxBatchSize) {
+ const batchSize = Math.min(maxBatchSize, coins.length - i);
+ const batchCoins = coins.slice(i, i + batchSize);
- logger.trace(`request body: ${j2s(reqBody)}`);
+ const depositSigsResp = await wex.cryptoApi.signPurseDeposits({
+ exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl,
+ pursePub: peerPushInitiation.pursePub,
+ coins: batchCoins,
+ });
- const httpResp = await wex.http.fetch(createPurseUrl.href, {
- method: "POST",
- body: reqBody,
- cancellationToken: wex.cancellationToken,
- });
+ if (i == 0) {
+ // First batch creates the purse!
- {
- const resp = await httpResp.json();
- logger.info(`resp: ${j2s(resp)}`);
- }
+ logger.trace(`encrypt contract request: ${j2s(encryptContractRequest)}`);
- switch (httpResp.status) {
- case HttpStatusCode.Ok:
- break;
- case HttpStatusCode.Forbidden: {
- // FIXME: Store this error!
- await ctx.failTransaction();
- return TaskRunResult.finished();
- }
- case HttpStatusCode.Conflict: {
- // Handle double-spending
- return handlePurseCreationConflict(wex, peerPushInitiation, httpResp);
- }
- default: {
- const errResp = await readTalerErrorResponse(httpResp);
- return {
- type: TaskRunResultType.Error,
- errorDetail: errResp,
+ const createPurseUrl = new URL(
+ `purses/${peerPushInitiation.pursePub}/create`,
+ peerPushInitiation.exchangeBaseUrl,
+ );
+
+ const reqBody = {
+ amount: peerPushInitiation.amount,
+ merge_pub: peerPushInitiation.mergePub,
+ purse_sig: purseSigResp.sig,
+ h_contract_terms: hContractTerms,
+ purse_expiration: timestampProtocolFromDb(purseExpiration),
+ deposits: depositSigsResp.deposits,
+ min_age: 0,
+ econtract: econtractResp.econtract,
};
+
+ if (logger.shouldLogTrace()) {
+ logger.trace(`request body: ${j2s(reqBody)}`);
+ }
+
+ const httpResp = await wex.http.fetch(createPurseUrl.href, {
+ method: "POST",
+ body: reqBody,
+ cancellationToken: wex.cancellationToken,
+ });
+
+ switch (httpResp.status) {
+ case HttpStatusCode.Ok:
+ // Possibly on to the next batch.
+ continue;
+ case HttpStatusCode.Forbidden: {
+ // FIXME: Store this error!
+ await ctx.failTransaction();
+ return TaskRunResult.finished();
+ }
+ case HttpStatusCode.Conflict: {
+ // Handle double-spending
+ return handlePurseCreationConflict(wex, peerPushInitiation, httpResp);
+ }
+ default: {
+ const errResp = await readTalerErrorResponse(httpResp);
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: errResp,
+ };
+ }
+ }
+ } else {
+ const purseDepositUrl = new URL(
+ `purses/${pursePub}/deposit`,
+ peerPushInitiation.exchangeBaseUrl,
+ );
+
+ const depositPayload: ExchangePurseDeposits = {
+ deposits: depositSigsResp.deposits,
+ };
+
+ const httpResp = await wex.http.fetch(purseDepositUrl.href, {
+ method: "POST",
+ body: depositPayload,
+ cancellationToken: wex.cancellationToken,
+ });
+
+ switch (httpResp.status) {
+ case HttpStatusCode.Ok:
+ // Possibly on to the next batch.
+ continue;
+ case HttpStatusCode.Forbidden: {
+ // FIXME: Store this error!
+ await ctx.failTransaction();
+ return TaskRunResult.finished();
+ }
+ case HttpStatusCode.Conflict: {
+ // Handle double-spending
+ return handlePurseCreationConflict(wex, peerPushInitiation, httpResp);
+ }
+ default: {
+ const errResp = await readTalerErrorResponse(httpResp);
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: errResp,
+ };
+ }
+ }
}
}
- if (httpResp.status !== HttpStatusCode.Ok) {
- // FIXME: do proper error reporting
- throw Error("got error response from exchange");
- }
+ // All batches done!
await transitionPeerPushDebitTransaction(wex, pursePub, {
stFrom: PeerPushDebitStatus.PendingCreatePurse,
@@ -585,13 +725,16 @@ async function processPeerPushDebitAbortingDeletePurse(
logger.info(`deleted purse with response status ${resp.status}`);
const transitionInfo = await wex.db.runReadWriteTx(
- [
- "peerPushDebit",
- "refreshGroups",
- "denominations",
- "coinAvailability",
- "coins",
- ],
+ {
+ storeNames: [
+ "peerPushDebit",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ ],
+ },
async (tx) => {
const ppiRec = await tx.peerPushDebit.get(pursePub);
if (!ppiRec) {
@@ -604,6 +747,10 @@ async function processPeerPushDebitAbortingDeletePurse(
const oldTxState = computePeerPushDebitTransactionState(ppiRec);
const coinPubs: CoinRefreshRequest[] = [];
+ if (!ppiRec.coinSel) {
+ return undefined;
+ }
+
for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
coinPubs.push({
amount: ppiRec.coinSel.contributions[i],
@@ -639,6 +786,7 @@ interface SimpleTransition {
stTo: PeerPushDebitStatus;
}
+// FIXME: This should be a transition on the peer push debit transaction context!
async function transitionPeerPushDebitTransaction(
wex: WalletExecutionContext,
pursePub: string,
@@ -649,7 +797,7 @@ async function transitionPeerPushDebitTransaction(
pursePub,
});
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushDebit"],
+ { storeNames: ["peerPushDebit"] },
async (tx) => {
const ppiRec = await tx.peerPushDebit.get(pursePub);
if (!ppiRec) {
@@ -686,7 +834,7 @@ async function processPeerPushDebitAbortingRefreshDeleted(
await waitRefreshFinal(wex, peerPushInitiation.abortRefreshGroupId);
}
const transitionInfo = await wex.db.runReadWriteTx(
- ["refreshGroups", "peerPushDebit"],
+ { storeNames: ["refreshGroups", "peerPushDebit"] },
async (tx) => {
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
let newOpState: PeerPushDebitStatus | undefined;
@@ -735,7 +883,7 @@ async function processPeerPushDebitAbortingRefreshExpired(
pursePub: peerPushInitiation.pursePub,
});
const transitionInfo = await wex.db.runReadWriteTx(
- ["peerPushDebit", "refreshGroups"],
+ { storeNames: ["peerPushDebit", "refreshGroups"] },
async (tx) => {
const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
let newOpState: PeerPushDebitStatus | undefined;
@@ -818,13 +966,16 @@ async function processPeerPushDebitReady(
} else if (resp.status === HttpStatusCode.Gone) {
logger.info(`purse ${pursePub} is gone, aborting peer-push-debit`);
const transitionInfo = await wex.db.runReadWriteTx(
- [
- "peerPushDebit",
- "refreshGroups",
- "denominations",
- "coinAvailability",
- "coins",
- ],
+ {
+ storeNames: [
+ "peerPushDebit",
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coinAvailability",
+ "coins",
+ ],
+ },
async (tx) => {
const ppiRec = await tx.peerPushDebit.get(pursePub);
if (!ppiRec) {
@@ -837,23 +988,26 @@ async function processPeerPushDebitReady(
const oldTxState = computePeerPushDebitTransactionState(ppiRec);
const coinPubs: CoinRefreshRequest[] = [];
- for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
- coinPubs.push({
- amount: ppiRec.coinSel.contributions[i],
- coinPub: ppiRec.coinSel.coinPubs[i],
- });
+ if (ppiRec.coinSel) {
+ for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
+ coinPubs.push({
+ amount: ppiRec.coinSel.contributions[i],
+ coinPub: ppiRec.coinSel.coinPubs[i],
+ });
+ }
+
+ const refresh = await createRefreshGroup(
+ wex,
+ tx,
+ currency,
+ coinPubs,
+ RefreshReason.AbortPeerPushDebit,
+ transactionId,
+ );
+
+ ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
}
-
- const refresh = await createRefreshGroup(
- wex,
- tx,
- currency,
- coinPubs,
- RefreshReason.AbortPeerPushDebit,
- transactionId,
- );
ppiRec.status = PeerPushDebitStatus.AbortingRefreshExpired;
- ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
await tx.peerPushDebit.put(ppiRec);
const newTxState = computePeerPushDebitTransactionState(ppiRec);
return {
@@ -875,7 +1029,7 @@ export async function processPeerPushDebit(
pursePub: string,
): Promise<TaskRunResult> {
const peerPushInitiation = await wex.db.runReadOnlyTx(
- ["peerPushDebit"],
+ { storeNames: ["peerPushDebit"] },
async (tx) => {
return tx.peerPushDebit.get(pursePub);
},
@@ -932,15 +1086,28 @@ export async function initiatePeerPushDebit(
const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({});
- const coinSelRes = await selectPeerCoins(wex, { instructedAmount });
+ const coinSelRes = await selectPeerCoins(wex, {
+ instructedAmount,
+ });
- if (coinSelRes.type !== "success") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
- {
- insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
- },
- );
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (coinSelRes.type) {
+ case "failure":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+ },
+ );
+ case "prospective":
+ coins = coinSelRes.result.prospectiveCoins;
+ break;
+ case "success":
+ coins = coinSelRes.result.coins;
+ break;
+ default:
+ assertUnreachable(coinSelRes);
}
const sel = coinSelRes.result;
@@ -948,10 +1115,7 @@ export async function initiatePeerPushDebit(
logger.info(`selected p2p coins (push):`);
logger.trace(`${j2s(coinSelRes)}`);
- const totalAmount = await getTotalPeerPaymentCost(
- wex,
- coinSelRes.result.coins,
- );
+ const totalAmount = await getTotalPeerPaymentCost(wex, coins);
logger.info(`computed total peer payment cost`);
@@ -964,31 +1128,19 @@ export async function initiatePeerPushDebit(
const contractEncNonce = encodeCrock(getRandomBytes(24));
const transitionInfo = await wex.db.runReadWriteTx(
- [
- "exchanges",
- "contractTerms",
- "coins",
- "coinAvailability",
- "denominations",
- "refreshGroups",
- "peerPushDebit",
- ],
+ {
+ storeNames: [
+ "exchanges",
+ "contractTerms",
+ "coins",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "peerPushDebit",
+ ],
+ },
async (tx) => {
- // FIXME: Instead of directly doing a spendCoin here,
- // we might want to mark the coins as used and spend them
- // after we've been able to create the purse.
- await spendCoins(wex, tx, {
- allocationId: constructTransactionIdentifier({
- tag: TransactionType.PeerPushDebit,
- pursePub: pursePair.pub,
- }),
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- ),
- refreshReason: RefreshReason.PayPeerPush,
- });
-
const ppi: PeerPushDebitRecord = {
amount: Amounts.stringify(instructedAmount),
contractPriv: contractKeyPair.priv,
@@ -1003,13 +1155,30 @@ export async function initiatePeerPushDebit(
timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
status: PeerPushDebitStatus.PendingCreatePurse,
contractEncNonce,
- coinSel: {
- coinPubs: sel.coins.map((x) => x.coinPub),
- contributions: sel.coins.map((x) => x.contribution),
- },
totalCost: Amounts.stringify(totalAmount),
};
+ if (coinSelRes.type === "success") {
+ ppi.coinSel = {
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) => x.contribution),
+ };
+ // FIXME: Instead of directly doing a spendCoin here,
+ // we might want to mark the coins as used and spend them
+ // after we've been able to create the purse.
+ await spendCoins(wex, tx, {
+ allocationId: constructTransactionIdentifier({
+ tag: TransactionType.PeerPushDebit,
+ pursePub: pursePair.pub,
+ }),
+ coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+ contributions: coinSelRes.result.coins.map((x) =>
+ Amounts.parseOrThrow(x.contribution),
+ ),
+ refreshReason: RefreshReason.PayPeerPush,
+ });
+ }
+
await tx.peerPushDebit.add(ppi);
await tx.contractTerms.put({
diff --git a/packages/taler-wallet-core/src/query.ts b/packages/taler-wallet-core/src/query.ts
index cffad84df..dc15bbdd1 100644
--- a/packages/taler-wallet-core/src/query.ts
+++ b/packages/taler-wallet-core/src/query.ts
@@ -15,8 +15,9 @@
*/
/**
- * Database query abstractions.
- * @module Query
+ * @fileoverview
+ * Query helpers for IndexedDB databases.
+ *
* @author Florian Dold
*/
@@ -31,10 +32,16 @@ import {
IDBKeyRange,
IDBRequest,
IDBTransaction,
+ IDBTransactionMode,
IDBValidKey,
IDBVersionChangeEvent,
} from "@gnu-taler/idb-bridge";
-import { Codec, Logger, openPromise } from "@gnu-taler/taler-util";
+import {
+ CancellationToken,
+ Codec,
+ Logger,
+ openPromise,
+} from "@gnu-taler/taler-util";
const logger = new Logger("query.ts");
@@ -557,6 +564,7 @@ function runTx<Arg, Res>(
tx: IDBTransaction,
arg: Arg,
f: (t: Arg, t2: IDBTransaction) => Promise<Res>,
+ triggerContext: InternalTriggerContext,
): Promise<Res> {
const stack = Error("Failed transaction was started here.");
return new Promise((resolve, reject) => {
@@ -577,6 +585,7 @@ function runTx<Arg, Res>(
logger.error(`${stack.stack}`);
reject(Error(msg));
}
+ triggerContext.handleAfterCommit();
resolve(funResult);
};
tx.onerror = () => {
@@ -621,6 +630,7 @@ function runTx<Arg, Res>(
function makeReadContext(
tx: IDBTransaction,
storePick: { [n: string]: StoreWithIndexes<any, any, any> },
+ triggerContext: InternalTriggerContext,
): any {
const ctx: { [s: string]: StoreReadOnlyAccessor<any, any> } = {};
for (const storeAlias in storePick) {
@@ -633,10 +643,12 @@ function makeReadContext(
const indexName = indexDescriptor.name;
indexes[indexAlias] = {
get(key) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).index(indexName).get(key);
return requestToPromise(req);
},
iter(query) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -644,6 +656,7 @@ function makeReadContext(
return new ResultStream<any>(req);
},
getAll(query, count) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -651,6 +664,7 @@ function makeReadContext(
return requestToPromise(req);
},
getAllKeys(query, count) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -658,6 +672,7 @@ function makeReadContext(
return requestToPromise(req);
},
count(query) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).index(indexName).count(query);
return requestToPromise(req);
},
@@ -666,14 +681,17 @@ function makeReadContext(
ctx[storeAlias] = {
indexes,
get(key) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).get(key);
return requestToPromise(req);
},
getAll(query, count) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).getAll(query, count);
return requestToPromise(req);
},
iter(query) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).openCursor(query);
return new ResultStream<any>(req);
},
@@ -685,6 +703,7 @@ function makeReadContext(
function makeWriteContext(
tx: IDBTransaction,
storePick: { [n: string]: StoreWithIndexes<any, any, any> },
+ triggerContext: InternalTriggerContext,
): any {
const ctx: { [s: string]: StoreReadWriteAccessor<any, any> } = {};
for (const storeAlias in storePick) {
@@ -697,10 +716,12 @@ function makeWriteContext(
const indexName = indexDescriptor.name;
indexes[indexAlias] = {
get(key) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).index(indexName).get(key);
return requestToPromise(req);
},
iter(query) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -708,6 +729,7 @@ function makeWriteContext(
return new ResultStream<any>(req);
},
getAll(query, count) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -715,6 +737,7 @@ function makeWriteContext(
return requestToPromise(req);
},
getAllKeys(query, count) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx
.objectStore(storeName)
.index(indexName)
@@ -722,6 +745,7 @@ function makeWriteContext(
return requestToPromise(req);
},
count(query) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).index(indexName).count(query);
return requestToPromise(req);
},
@@ -730,18 +754,23 @@ function makeWriteContext(
ctx[storeAlias] = {
indexes,
get(key) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).get(key);
return requestToPromise(req);
},
getAll(query, count) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).getAll(query, count);
return requestToPromise(req);
},
iter(query) {
+ triggerContext.storesAccessed.add(storeName);
const req = tx.objectStore(storeName).openCursor(query);
return new ResultStream<any>(req);
},
async add(r, k) {
+ triggerContext.storesAccessed.add(storeName);
+ triggerContext.storesModified.add(storeName);
const req = tx.objectStore(storeName).add(r, k);
const key = await requestToPromise(req);
return {
@@ -749,6 +778,8 @@ function makeWriteContext(
};
},
async put(r, k) {
+ triggerContext.storesAccessed.add(storeName);
+ triggerContext.storesModified.add(storeName);
const req = tx.objectStore(storeName).put(r, k);
const key = await requestToPromise(req);
return {
@@ -756,6 +787,8 @@ function makeWriteContext(
};
},
delete(k) {
+ triggerContext.storesAccessed.add(storeName);
+ triggerContext.storesModified.add(storeName);
const req = tx.objectStore(storeName).delete(k);
return requestToPromise(req);
},
@@ -766,6 +799,7 @@ function makeWriteContext(
export interface DbAccess<StoreMap> {
idbHandle(): IDBDatabase;
+
runAllStoresReadWriteTx<T>(
options: {
label?: string;
@@ -774,6 +808,7 @@ export interface DbAccess<StoreMap> {
tx: DbReadWriteTransaction<StoreMap, Array<StoreNames<StoreMap>>>,
) => Promise<T>,
): Promise<T>;
+
runAllStoresReadOnlyTx<T>(
options: {
label?: string;
@@ -782,16 +817,67 @@ export interface DbAccess<StoreMap> {
tx: DbReadOnlyTransaction<StoreMap, Array<StoreNames<StoreMap>>>,
) => Promise<T>,
): Promise<T>;
+
runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
- storeNames: StoreNameArray,
+ opts: {
+ storeNames: StoreNameArray;
+ label?: string;
+ },
txf: (tx: DbReadWriteTransaction<StoreMap, StoreNameArray>) => Promise<T>,
): Promise<T>;
+
runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
- storeNames: StoreNameArray,
+ opts: {
+ storeNames: StoreNameArray;
+ label?: string;
+ },
txf: (tx: DbReadOnlyTransaction<StoreMap, StoreNameArray>) => Promise<T>,
): Promise<T>;
}
+export interface AfterCommitInfo {
+ mode: IDBTransactionMode;
+ scope: Set<string>;
+ accessedStores: Set<string>;
+ modifiedStores: Set<string>;
+}
+
+export interface TriggerSpec {
+ /**
+ * Trigger run after every successful commit, run outside of the transaction.
+ */
+ afterCommit?: (info: AfterCommitInfo) => void;
+
+ // onRead(store, value)
+ // initState<State> () => State
+ // beforeCommit<State>? (tx: Transaction, s: State | undefined) => Promise<void>;
+}
+
+class InternalTriggerContext {
+ storesScope: Set<string>;
+ storesAccessed: Set<string> = new Set();
+ storesModified: Set<string> = new Set();
+
+ constructor(
+ private triggerSpec: TriggerSpec,
+ private mode: IDBTransactionMode,
+ scope: string[],
+ ) {
+ this.storesScope = new Set(scope);
+ }
+
+ handleAfterCommit() {
+ if (this.triggerSpec.afterCommit) {
+ this.triggerSpec.afterCommit({
+ mode: this.mode,
+ accessedStores: this.storesAccessed,
+ modifiedStores: this.storesModified,
+ scope: this.storesScope,
+ });
+ }
+ }
+}
+
/**
* Type-safe access to a database with a particular store map.
*
@@ -801,6 +887,8 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
constructor(
private db: IDBDatabase,
private stores: StoreMap,
+ private triggers: TriggerSpec = {},
+ private cancellationToken: CancellationToken,
) {}
idbHandle(): IDBDatabase {
@@ -823,12 +911,18 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
strStoreNames.push(swi.storeName);
accessibleStores[swi.storeName] = swi;
}
- const tx = this.db.transaction(strStoreNames, "readwrite");
- const writeContext = makeWriteContext(tx, accessibleStores);
- return runTx(tx, writeContext, txf);
+ const mode = "readwrite";
+ const triggerContext = new InternalTriggerContext(
+ this.triggers,
+ mode,
+ strStoreNames,
+ );
+ const tx = this.db.transaction(strStoreNames, mode);
+ const writeContext = makeWriteContext(tx, accessibleStores, triggerContext);
+ return runTx(tx, writeContext, txf, triggerContext);
}
- runAllStoresReadOnlyTx<T>(
+ async runAllStoresReadOnlyTx<T>(
options: {
label?: string;
},
@@ -844,42 +938,67 @@ export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> {
strStoreNames.push(swi.storeName);
accessibleStores[swi.storeName] = swi;
}
- const tx = this.db.transaction(strStoreNames, "readonly");
- const writeContext = makeReadContext(tx, accessibleStores);
- return runTx(tx, writeContext, txf);
+ const mode = "readonly";
+ const triggerContext = new InternalTriggerContext(
+ this.triggers,
+ mode,
+ strStoreNames,
+ );
+ const tx = this.db.transaction(strStoreNames, mode);
+ const writeContext = makeReadContext(tx, accessibleStores, triggerContext);
+ const res = await runTx(tx, writeContext, txf, triggerContext);
+ return res;
}
- runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
- storeNames: StoreNameArray,
+ async runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
+ opts: {
+ storeNames: StoreNameArray;
+ },
txf: (tx: DbReadWriteTransaction<StoreMap, StoreNameArray>) => Promise<T>,
): Promise<T> {
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
{};
const strStoreNames: string[] = [];
- for (const sn of storeNames) {
+ for (const sn of opts.storeNames) {
const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
strStoreNames.push(swi.storeName);
accessibleStores[swi.storeName] = swi;
}
- const tx = this.db.transaction(strStoreNames, "readwrite");
- const writeContext = makeWriteContext(tx, accessibleStores);
- return runTx(tx, writeContext, txf);
+ const mode = "readwrite";
+ const triggerContext = new InternalTriggerContext(
+ this.triggers,
+ mode,
+ strStoreNames,
+ );
+ const tx = this.db.transaction(strStoreNames, mode);
+ const writeContext = makeWriteContext(tx, accessibleStores, triggerContext);
+ const res = await runTx(tx, writeContext, txf, triggerContext);
+ return res;
}
runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
- storeNames: StoreNameArray,
+ opts: {
+ storeNames: StoreNameArray;
+ },
txf: (tx: DbReadOnlyTransaction<StoreMap, StoreNameArray>) => Promise<T>,
): Promise<T> {
const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
{};
const strStoreNames: string[] = [];
- for (const sn of storeNames) {
+ for (const sn of opts.storeNames) {
const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
strStoreNames.push(swi.storeName);
accessibleStores[swi.storeName] = swi;
}
- const tx = this.db.transaction(strStoreNames, "readonly");
- const readContext = makeReadContext(tx, accessibleStores);
- return runTx(tx, readContext, txf);
+ const mode = "readonly";
+ const triggerContext = new InternalTriggerContext(
+ this.triggers,
+ mode,
+ strStoreNames,
+ );
+ const tx = this.db.transaction(strStoreNames, mode);
+ const readContext = makeReadContext(tx, accessibleStores, triggerContext);
+ const res = runTx(tx, readContext, txf, triggerContext);
+ return res;
}
}
diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts
index 8d5d3dd1f..6a09f9a0e 100644
--- a/packages/taler-wallet-core/src/recoup.ts
+++ b/packages/taler-wallet-core/src/recoup.ts
@@ -26,7 +26,6 @@
*/
import {
Amounts,
- CancellationToken,
CoinStatus,
Logger,
RefreshReason,
@@ -63,11 +62,7 @@ import {
} from "./db.js";
import { createRefreshGroup } from "./refresh.js";
import { constructTransactionIdentifier } from "./transactions.js";
-import {
- WalletExecutionContext,
- getDenomInfo,
- type InternalWalletState,
-} from "./wallet.js";
+import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
import { internalCreateWithdrawalGroup } from "./withdraw.js";
export const logger = new Logger("operations/recoup.ts");
@@ -104,7 +99,7 @@ async function recoupRewardCoin(
// Thus we just put the coin to sleep.
// FIXME: somehow report this to the user
await wex.db.runReadWriteTx(
- ["recoupGroups", "denominations", "refreshGroups", "coins"],
+ { storeNames: ["recoupGroups", "denominations", "refreshGroups", "coins"] },
async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) {
@@ -126,7 +121,7 @@ async function recoupRefreshCoin(
cs: RefreshCoinSource,
): Promise<void> {
const d = await wex.db.runReadOnlyTx(
- ["coins", "denominations"],
+ { storeNames: ["coins", "denominations"] },
async (tx) => {
const denomInfo = await getDenomInfo(
wex,
@@ -173,7 +168,7 @@ async function recoupRefreshCoin(
}
await wex.db.runReadWriteTx(
- ["coins", "denominations", "recoupGroups", "refreshGroups"],
+ { storeNames: ["coins", "denominations", "recoupGroups", "refreshGroups"] },
async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) {
@@ -237,15 +232,18 @@ export async function recoupWithdrawCoin(
cs: WithdrawCoinSource,
): Promise<void> {
const reservePub = cs.reservePub;
- const denomInfo = await wex.db.runReadOnlyTx(["denominations"], async (tx) => {
- const denomInfo = await getDenomInfo(
- wex,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- return denomInfo;
- });
+ const denomInfo = await wex.db.runReadOnlyTx(
+ { storeNames: ["denominations"] },
+ async (tx) => {
+ const denomInfo = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ return denomInfo;
+ },
+ );
if (!denomInfo) {
// FIXME: We should at least emit some pending operation / warning for this?
return;
@@ -278,7 +276,7 @@ export async function recoupWithdrawCoin(
// FIXME: verify that our expectations about the amount match
await wex.db.runReadWriteTx(
- ["coins", "denominations", "recoupGroups", "refreshGroups"],
+ { storeNames: ["coins", "denominations", "recoupGroups", "refreshGroups"] },
async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) {
@@ -302,9 +300,12 @@ export async function processRecoupGroup(
wex: WalletExecutionContext,
recoupGroupId: string,
): Promise<TaskRunResult> {
- let recoupGroup = await wex.db.runReadOnlyTx(["recoupGroups"], async (tx) => {
- return tx.recoupGroups.get(recoupGroupId);
- });
+ let recoupGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["recoupGroups"] },
+ async (tx) => {
+ return tx.recoupGroups.get(recoupGroupId);
+ },
+ );
if (!recoupGroup) {
return TaskRunResult.finished();
}
@@ -322,9 +323,12 @@ export async function processRecoupGroup(
});
await Promise.all(ps);
- recoupGroup = await wex.db.runReadOnlyTx(["recoupGroups"], async (tx) => {
- return tx.recoupGroups.get(recoupGroupId);
- });
+ recoupGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["recoupGroups"] },
+ async (tx) => {
+ return tx.recoupGroups.get(recoupGroupId);
+ },
+ );
if (!recoupGroup) {
return TaskRunResult.finished();
}
@@ -341,22 +345,25 @@ export async function processRecoupGroup(
const reservePrivMap: Record<string, string> = {};
for (let i = 0; i < recoupGroup.coinPubs.length; i++) {
const coinPub = recoupGroup.coinPubs[i];
- await wex.db.runReadOnlyTx(["coins", "reserves"], async (tx) => {
- const coin = await tx.coins.get(coinPub);
- if (!coin) {
- throw Error(`Coin ${coinPub} not found, can't request recoup`);
- }
- if (coin.coinSource.type === CoinSourceType.Withdraw) {
- const reserve = await tx.reserves.indexes.byReservePub.get(
- coin.coinSource.reservePub,
- );
- if (!reserve) {
- return;
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "reserves"] },
+ async (tx) => {
+ const coin = await tx.coins.get(coinPub);
+ if (!coin) {
+ throw Error(`Coin ${coinPub} not found, can't request recoup`);
}
- reserveSet.add(coin.coinSource.reservePub);
- reservePrivMap[coin.coinSource.reservePub] = reserve.reservePriv;
- }
- });
+ if (coin.coinSource.type === CoinSourceType.Withdraw) {
+ const reserve = await tx.reserves.indexes.byReservePub.get(
+ coin.coinSource.reservePub,
+ );
+ if (!reserve) {
+ return;
+ }
+ reserveSet.add(coin.coinSource.reservePub);
+ reservePrivMap[coin.coinSource.reservePub] = reserve.reservePriv;
+ }
+ },
+ );
}
for (const reservePub of reserveSet) {
@@ -387,13 +394,16 @@ export async function processRecoupGroup(
}
await wex.db.runReadWriteTx(
- [
- "recoupGroups",
- "coinAvailability",
- "denominations",
- "refreshGroups",
- "coins",
- ],
+ {
+ storeNames: [
+ "recoupGroups",
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ ],
+ },
async (tx) => {
const rg2 = await tx.recoupGroups.get(recoupGroupId);
if (!rg2) {
@@ -420,7 +430,7 @@ export async function processRecoupGroup(
return TaskRunResult.finished();
}
-export class RewardTransactionContext implements TransactionContext {
+export class RecoupTransactionContext implements TransactionContext {
abortTransaction(): Promise<void> {
throw new Error("Method not implemented.");
}
@@ -440,7 +450,7 @@ export class RewardTransactionContext implements TransactionContext {
public taskId: TaskIdStr;
constructor(
- public ws: InternalWalletState,
+ public wex: WalletExecutionContext,
private recoupGroupId: string,
) {
this.transactionId = constructTransactionIdentifier({
@@ -487,6 +497,10 @@ export async function createRecoupGroup(
await tx.recoupGroups.put(recoupGroup);
+ const ctx = new RecoupTransactionContext(wex, recoupGroupId);
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
return recoupGroupId;
}
@@ -499,7 +513,7 @@ async function processRecoupForCoin(
coinIdx: number,
): Promise<void> {
const coin = await wex.db.runReadOnlyTx(
- ["coins", "recoupGroups"],
+ { storeNames: ["coins", "recoupGroups"] },
async (tx) => {
const recoupGroup = await tx.recoupGroups.get(recoupGroupId);
if (!recoupGroup) {
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
index 7c9ec84bd..7800967e6 100644
--- a/packages/taler-wallet-core/src/refresh.ts
+++ b/packages/taler-wallet-core/src/refresh.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2019-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,6 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+/**
+ * @fileoverview
+ * Implementation of the refresh transaction.
+ */
+
+/**
+ * Imports.
+ */
import {
AgeCommitment,
AgeRestriction,
@@ -23,6 +31,7 @@ import {
assertUnreachable,
AsyncFlag,
checkDbInvariant,
+ codecForCoinHistoryResponse,
codecForExchangeMeltResponse,
codecForExchangeRevealResponse,
CoinPublicKeyString,
@@ -61,12 +70,10 @@ import {
import {
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
- readUnexpectedResponseDetails,
throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
constructTaskIdentifier,
- makeCoinAvailable,
makeCoinsVisible,
PendingTaskType,
TaskIdStr,
@@ -74,6 +81,8 @@ import {
TaskRunResultType,
TombstoneTag,
TransactionContext,
+ TransitionResult,
+ TransitionResultType,
} from "./common.js";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import {
@@ -82,6 +91,7 @@ import {
} from "./crypto/cryptoTypes.js";
import { CryptoApiStoppedError } from "./crypto/workers/crypto-dispatcher.js";
import {
+ CoinAvailabilityRecord,
CoinRecord,
CoinSourceType,
DenominationRecord,
@@ -93,25 +103,40 @@ import {
timestampPreciseToDb,
WalletDbReadOnlyTransaction,
WalletDbReadWriteTransaction,
+ WalletDbStoresArr,
} from "./db.js";
import { selectWithdrawalDenominations } from "./denomSelection.js";
-import { fetchFreshExchange } from "./exchanges.js";
import {
constructTransactionIdentifier,
notifyTransition,
+ TransitionInfo,
} from "./transactions.js";
import {
EXCHANGE_COINS_LOCK,
getDenomInfo,
WalletExecutionContext,
} from "./wallet.js";
-import {
- getCandidateWithdrawalDenomsTx,
- updateWithdrawalDenoms,
-} from "./withdraw.js";
+import { getCandidateWithdrawalDenomsTx } from "./withdraw.js";
const logger = new Logger("refresh.ts");
+/**
+ * Update the materialized refresh transaction based
+ * on the refresh group record.
+ */
+async function updateRefreshTransaction(
+ ctx: RefreshTransactionContext,
+ tx: WalletDbReadWriteTransaction<
+ [
+ "refreshGroups",
+ "transactions",
+ "operationRetries",
+ "exchanges",
+ "exchangeDetails",
+ ]
+ >,
+): Promise<void> {}
+
export class RefreshTransactionContext implements TransactionContext {
readonly transactionId: TransactionIdStr;
readonly taskId: TaskIdStr;
@@ -130,56 +155,112 @@ export class RefreshTransactionContext implements TransactionContext {
});
}
- async deleteTransaction(): Promise<void> {
- const refreshGroupId = this.refreshGroupId;
- await this.wex.db.runReadWriteTx(
- ["refreshGroups", "tombstones"],
+ /**
+ * Transition a withdrawal transaction.
+ * Extra object stores may be accessed during the transition.
+ */
+ async transition<StoreNameArray extends WalletDbStoresArr = []>(
+ opts: { extraStores?: StoreNameArray; transactionLabel?: string },
+ f: (
+ rec: RefreshGroupRecord | undefined,
+ tx: WalletDbReadWriteTransaction<
+ [
+ "refreshGroups",
+ "transactions",
+ "operationRetries",
+ "exchanges",
+ "exchangeDetails",
+ ...StoreNameArray,
+ ]
+ >,
+ ) => Promise<TransitionResult<RefreshGroupRecord>>,
+ ): Promise<TransitionInfo | undefined> {
+ const baseStores = [
+ "refreshGroups" as const,
+ "transactions" as const,
+ "operationRetries" as const,
+ "exchanges" as const,
+ "exchangeDetails" as const,
+ ];
+ let stores = opts.extraStores
+ ? [...baseStores, ...opts.extraStores]
+ : baseStores;
+ const transitionInfo = await this.wex.db.runReadWriteTx(
+ { storeNames: stores },
async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (rg) {
- await tx.refreshGroups.delete(refreshGroupId);
- await tx.tombstones.put({
- id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
- });
+ const wgRec = await tx.refreshGroups.get(this.refreshGroupId);
+ let oldTxState: TransactionState;
+ if (wgRec) {
+ oldTxState = computeRefreshTransactionState(wgRec);
+ } else {
+ oldTxState = {
+ major: TransactionMajorState.None,
+ };
+ }
+ const res = await f(wgRec, tx);
+ switch (res.type) {
+ case TransitionResultType.Transition: {
+ await tx.refreshGroups.put(res.rec);
+ await updateRefreshTransaction(this, tx);
+ const newTxState = computeRefreshTransactionState(res.rec);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ case TransitionResultType.Delete:
+ await tx.refreshGroups.delete(this.refreshGroupId);
+ await updateRefreshTransaction(this, tx);
+ return {
+ oldTxState,
+ newTxState: {
+ major: TransactionMajorState.None,
+ },
+ };
+ default:
+ return undefined;
}
},
);
+ notifyTransition(this.wex, this.transactionId, transitionInfo);
+ return transitionInfo;
}
- async suspendTransaction(): Promise<void> {
- const { wex, refreshGroupId, transactionId } = this;
- let transitionInfo = await wex.db.runReadWriteTx(
- ["refreshGroups"],
- async (tx) => {
- const dg = await tx.refreshGroups.get(refreshGroupId);
- if (!dg) {
- logger.warn(
- `can't suspend refresh group, refreshGroupId=${refreshGroupId} not found`,
- );
- return undefined;
- }
- const oldState = computeRefreshTransactionState(dg);
- switch (dg.operationStatus) {
- case RefreshOperationStatus.Finished:
- case RefreshOperationStatus.Suspended:
- case RefreshOperationStatus.Failed:
- return undefined;
- case RefreshOperationStatus.Pending: {
- dg.operationStatus = RefreshOperationStatus.Suspended;
- await tx.refreshGroups.put(dg);
- break;
- }
- default:
- assertUnreachable(dg.operationStatus);
+ async deleteTransaction(): Promise<void> {
+ await this.transition(
+ {
+ extraStores: ["tombstones"],
+ },
+ async (rec, tx) => {
+ if (!rec) {
+ return TransitionResult.stay();
}
- return {
- oldTxState: oldState,
- newTxState: computeRefreshTransactionState(dg),
- };
+ await tx.tombstones.put({
+ id: TombstoneTag.DeleteRefreshGroup + ":" + this.refreshGroupId,
+ });
+ return TransitionResult.delete();
},
);
- wex.taskScheduler.stopShepherdTask(this.taskId);
- notifyTransition(wex, transactionId, transitionInfo);
+ }
+
+ async suspendTransaction(): Promise<void> {
+ await this.transition({}, async (rec, tx) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ switch (rec.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ case RefreshOperationStatus.Suspended:
+ case RefreshOperationStatus.Failed:
+ return TransitionResult.stay();
+ case RefreshOperationStatus.Pending: {
+ rec.operationStatus = RefreshOperationStatus.Suspended;
+ return TransitionResult.transition(rec);
+ }
+ default:
+ assertUnreachable(rec.operationStatus);
+ }
+ });
}
async abortTransaction(): Promise<void> {
@@ -188,81 +269,74 @@ export class RefreshTransactionContext implements TransactionContext {
}
async resumeTransaction(): Promise<void> {
- const { wex, refreshGroupId, transactionId } = this;
- const transitionInfo = await wex.db.runReadWriteTx(
- ["refreshGroups"],
- async (tx) => {
- const dg = await tx.refreshGroups.get(refreshGroupId);
- if (!dg) {
- logger.warn(
- `can't resume refresh group, refreshGroupId=${refreshGroupId} not found`,
- );
- return;
- }
- const oldState = computeRefreshTransactionState(dg);
- switch (dg.operationStatus) {
- case RefreshOperationStatus.Finished:
- return;
- case RefreshOperationStatus.Pending: {
- return;
- }
- case RefreshOperationStatus.Suspended:
- dg.operationStatus = RefreshOperationStatus.Pending;
- await tx.refreshGroups.put(dg);
- return {
- oldTxState: oldState,
- newTxState: computeRefreshTransactionState(dg),
- };
+ await this.transition({}, async (rec, tx) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ switch (rec.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ case RefreshOperationStatus.Failed:
+ case RefreshOperationStatus.Pending:
+ return TransitionResult.stay();
+ case RefreshOperationStatus.Suspended: {
+ rec.operationStatus = RefreshOperationStatus.Pending;
+ return TransitionResult.transition(rec);
}
- return undefined;
- },
- );
- notifyTransition(wex, transactionId, transitionInfo);
- wex.taskScheduler.startShepherdTask(this.taskId);
+ default:
+ assertUnreachable(rec.operationStatus);
+ }
+ });
}
async failTransaction(): Promise<void> {
- const { wex, refreshGroupId, transactionId } = this;
- const transitionInfo = await wex.db.runReadWriteTx(
- ["refreshGroups"],
- async (tx) => {
- const dg = await tx.refreshGroups.get(refreshGroupId);
- if (!dg) {
- logger.warn(
- `can't resume refresh group, refreshGroupId=${refreshGroupId} not found`,
- );
- return;
- }
- const oldState = computeRefreshTransactionState(dg);
- let newStatus: RefreshOperationStatus | undefined;
- switch (dg.operationStatus) {
- case RefreshOperationStatus.Finished:
- break;
- case RefreshOperationStatus.Pending:
- case RefreshOperationStatus.Suspended:
- newStatus = RefreshOperationStatus.Failed;
- break;
- case RefreshOperationStatus.Failed:
- break;
- default:
- assertUnreachable(dg.operationStatus);
- }
- if (newStatus) {
- dg.operationStatus = newStatus;
- await tx.refreshGroups.put(dg);
+ await this.transition({}, async (rec, tx) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ switch (rec.operationStatus) {
+ case RefreshOperationStatus.Finished:
+ case RefreshOperationStatus.Failed:
+ return TransitionResult.stay();
+ case RefreshOperationStatus.Pending:
+ case RefreshOperationStatus.Suspended: {
+ rec.operationStatus = RefreshOperationStatus.Failed;
+ return TransitionResult.transition(rec);
}
- return {
- oldTxState: oldState,
- newTxState: computeRefreshTransactionState(dg),
- };
- },
- );
- wex.taskScheduler.stopShepherdTask(this.taskId);
- notifyTransition(wex, transactionId, transitionInfo);
- wex.taskScheduler.startShepherdTask(this.taskId);
+ default:
+ assertUnreachable(rec.operationStatus);
+ }
+ });
}
}
+export async function getTotalRefreshCost(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["denominations"]>,
+ refreshedDenom: DenominationInfo,
+ amountLeft: AmountJson,
+): Promise<AmountJson> {
+ const cacheKey = `denom=${refreshedDenom.exchangeBaseUrl}/${
+ refreshedDenom.denomPubHash
+ };left=${Amounts.stringify(amountLeft)}`;
+ const cacheRes = wex.ws.refreshCostCache.get(cacheKey);
+ if (cacheRes) {
+ return cacheRes;
+ }
+ const allDenoms = await getCandidateWithdrawalDenomsTx(
+ wex,
+ tx,
+ refreshedDenom.exchangeBaseUrl,
+ Amounts.currencyOf(amountLeft),
+ );
+ const res = getTotalRefreshCostInternal(
+ allDenoms,
+ refreshedDenom,
+ amountLeft,
+ );
+ wex.ws.refreshCostCache.put(cacheKey, res);
+ return res;
+}
+
/**
* Get the amount that we lose when refreshing a coin of the given denomination
* with a certain amount left.
@@ -274,11 +348,10 @@ export class RefreshTransactionContext implements TransactionContext {
* Considers refresh fees, withdrawal fees after refresh and amounts too small
* to refresh.
*/
-export function getTotalRefreshCost(
+export function getTotalRefreshCostInternal(
denoms: DenominationRecord[],
refreshedDenom: DenominationInfo,
amountLeft: AmountJson,
- denomselAllowLate: boolean,
): AmountJson {
const withdrawAmount = Amounts.sub(
amountLeft,
@@ -288,7 +361,7 @@ export function getTotalRefreshCost(
const withdrawDenoms = selectWithdrawalDenominations(
withdrawAmount,
denoms,
- denomselAllowLate,
+ false,
);
const resultingAmount = Amounts.add(
Amounts.zeroOfCurrency(withdrawAmount.currency),
@@ -305,192 +378,170 @@ export function getTotalRefreshCost(
return totalCost;
}
-function updateGroupStatus(rg: RefreshGroupRecord): { final: boolean } {
- const allFinal = fnutil.all(
- rg.statusPerCoin,
- (x) => x === RefreshCoinStatus.Finished || x === RefreshCoinStatus.Failed,
- );
- const anyFailed = fnutil.any(
- rg.statusPerCoin,
- (x) => x === RefreshCoinStatus.Failed,
- );
- if (allFinal) {
- if (anyFailed) {
- rg.timestampFinished = timestampPreciseToDb(TalerPreciseTimestamp.now());
- rg.operationStatus = RefreshOperationStatus.Failed;
- } else {
- rg.timestampFinished = timestampPreciseToDb(TalerPreciseTimestamp.now());
- rg.operationStatus = RefreshOperationStatus.Finished;
- }
- return { final: true };
+async function getCoinAvailabilityForDenom(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["coins", "coinAvailability", "denominations"]
+ >,
+ denom: DenominationInfo,
+ ageRestriction: number,
+): Promise<CoinAvailabilityRecord> {
+ checkDbInvariant(!!denom);
+ let car = await tx.coinAvailability.get([
+ denom.exchangeBaseUrl,
+ denom.denomPubHash,
+ ageRestriction,
+ ]);
+ if (!car) {
+ car = {
+ maxAge: ageRestriction,
+ value: denom.value,
+ currency: Amounts.currencyOf(denom.value),
+ denomPubHash: denom.denomPubHash,
+ exchangeBaseUrl: denom.exchangeBaseUrl,
+ freshCoinCount: 0,
+ visibleCoinCount: 0,
+ };
}
- return { final: false };
+ return car;
}
/**
* Create a refresh session for one particular coin inside a refresh group.
- *
- * If the session already exists, return the existing one.
- *
- * If the session doesn't need to be created (refresh group gone or session already
- * finished), return undefined.
*/
-async function provideRefreshSession(
+async function initRefreshSession(
wex: WalletExecutionContext,
- refreshGroupId: string,
+ tx: WalletDbReadWriteTransaction<
+ ["refreshSessions", "coinAvailability", "coins", "denominations"]
+ >,
+ refreshGroup: RefreshGroupRecord,
coinIndex: number,
-): Promise<RefreshSessionRecord | undefined> {
+): Promise<void> {
+ const refreshGroupId = refreshGroup.refreshGroupId;
logger.trace(
`creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`,
);
-
- const d = await wex.db.runReadWriteTx(
- ["coins", "refreshGroups", "refreshSessions"],
- async (tx) => {
- const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
- if (!refreshGroup) {
- return;
- }
- if (
- refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Finished
- ) {
- return;
- }
- const existingRefreshSession = await tx.refreshSessions.get([
- refreshGroupId,
- coinIndex,
- ]);
- const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex];
- const coin = await tx.coins.get(oldCoinPub);
- if (!coin) {
- throw Error("Can't refresh, coin not found");
- }
- return { refreshGroup, coin, existingRefreshSession };
- },
- );
-
- if (!d) {
- return undefined;
- }
-
- if (d.existingRefreshSession) {
- return d.existingRefreshSession;
+ const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex];
+ const oldCoin = await tx.coins.get(oldCoinPub);
+ if (!oldCoin) {
+ throw Error("Can't refresh, coin not found");
}
- const { refreshGroup, coin } = d;
-
- const exch = await fetchFreshExchange(wex, coin.exchangeBaseUrl);
+ const exchangeBaseUrl = oldCoin.exchangeBaseUrl;
- // FIXME: use helper functions from withdraw.ts
- // to update and filter withdrawable denoms.
+ const sessionSecretSeed = encodeCrock(getRandomBytes(64));
- const { availableAmount, availableDenoms } = await wex.db.runReadOnlyTx(
- ["denominations"],
- async (tx) => {
- const oldDenom = await getDenomInfo(
- wex,
- tx,
- exch.exchangeBaseUrl,
- coin.denomPubHash,
- );
+ const oldDenom = await getDenomInfo(
+ wex,
+ tx,
+ exchangeBaseUrl,
+ oldCoin.denomPubHash,
+ );
- if (!oldDenom) {
- throw Error("db inconsistent: denomination for coin not found");
- }
+ if (!oldDenom) {
+ throw Error("db inconsistent: denomination for coin not found");
+ }
- // FIXME: Use denom groups instead of querying all denominations!
- const availableDenoms: DenominationRecord[] =
- await tx.denominations.indexes.byExchangeBaseUrl.getAll(
- exch.exchangeBaseUrl,
- );
+ const currency = refreshGroup.currency;
- const availableAmount = Amounts.sub(
- refreshGroup.inputPerCoin[coinIndex],
- oldDenom.feeRefresh,
- ).amount;
- return { availableAmount, availableDenoms };
- },
+ const availableDenoms = await getCandidateWithdrawalDenomsTx(
+ wex,
+ tx,
+ exchangeBaseUrl,
+ currency,
);
+ const availableAmount = Amounts.sub(
+ refreshGroup.inputPerCoin[coinIndex],
+ oldDenom.feeRefresh,
+ ).amount;
+
const newCoinDenoms = selectWithdrawalDenominations(
availableAmount,
availableDenoms,
wex.ws.config.testing.denomselAllowLate,
);
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Refresh,
- refreshGroupId,
- });
-
if (newCoinDenoms.selectedDenoms.length === 0) {
logger.trace(
`not refreshing, available amount ${amountToPretty(
availableAmount,
)} too small`,
);
- const transitionInfo = await wex.db.runReadWriteTx(
- ["refreshGroups", "coins", "coinAvailability"],
- async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- const oldTxState = computeRefreshTransactionState(rg);
- rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
- const updateRes = updateGroupStatus(rg);
- if (updateRes.final) {
- await makeCoinsVisible(wex, tx, transactionId);
- }
- await tx.refreshGroups.put(rg);
- const newTxState = computeRefreshTransactionState(rg);
- return { oldTxState, newTxState };
- },
- );
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
- notifyTransition(wex, transactionId, transitionInfo);
+ refreshGroup.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
return;
}
- const sessionSecretSeed = encodeCrock(getRandomBytes(64));
+ for (let i = 0; i < newCoinDenoms.selectedDenoms.length; i++) {
+ const dph = newCoinDenoms.selectedDenoms[i].denomPubHash;
+ const denom = await getDenomInfo(wex, tx, oldDenom.exchangeBaseUrl, dph);
+ if (!denom) {
+ logger.error(`denom ${dph} not in DB`);
+ continue;
+ }
+ const car = await getCoinAvailabilityForDenom(
+ wex,
+ tx,
+ denom,
+ oldCoin.maxAge,
+ );
+ car.pendingRefreshOutputCount =
+ (car.pendingRefreshOutputCount ?? 0) +
+ newCoinDenoms.selectedDenoms[i].count;
+ await tx.coinAvailability.put(car);
+ }
- // Store refresh session for this coin in the database.
- const mySession = await wex.db.runReadWriteTx(
- ["refreshGroups", "refreshSessions"],
- async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- const existingSession = await tx.refreshSessions.get([
- refreshGroupId,
- coinIndex,
- ]);
- if (existingSession) {
- return existingSession;
- }
- const newSession: RefreshSessionRecord = {
- coinIndex,
- refreshGroupId,
- norevealIndex: undefined,
- sessionSecretSeed: sessionSecretSeed,
- newDenoms: newCoinDenoms.selectedDenoms.map((x) => ({
- count: x.count,
- denomPubHash: x.denomPubHash,
- })),
- amountRefreshOutput: Amounts.stringify(newCoinDenoms.totalCoinValue),
- };
- await tx.refreshSessions.put(newSession);
- return newSession;
- },
- );
- logger.trace(
- `found/created refresh session for coin #${coinIndex} in ${refreshGroupId}`,
- );
- return mySession;
+ const newSession: RefreshSessionRecord = {
+ coinIndex,
+ refreshGroupId,
+ norevealIndex: undefined,
+ sessionSecretSeed: sessionSecretSeed,
+ newDenoms: newCoinDenoms.selectedDenoms.map((x) => ({
+ count: x.count,
+ denomPubHash: x.denomPubHash,
+ })),
+ amountRefreshOutput: Amounts.stringify(newCoinDenoms.totalCoinValue),
+ };
+ await tx.refreshSessions.put(newSession);
+}
+
+/**
+ * Uninitialize a refresh session.
+ *
+ * Adjust the coin availability of involved coins.
+ */
+async function destroyRefreshSession(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadWriteTransaction<
+ ["denominations", "coinAvailability", "coins"]
+ >,
+ refreshGroup: RefreshGroupRecord,
+ refreshSession: RefreshSessionRecord,
+): Promise<void> {
+ for (let i = 0; i < refreshSession.newDenoms.length; i++) {
+ const oldCoin = await tx.coins.get(
+ refreshGroup.oldCoinPubs[refreshSession.coinIndex],
+ );
+ if (!oldCoin) {
+ continue;
+ }
+ const dph = refreshSession.newDenoms[i].denomPubHash;
+ const denom = await getDenomInfo(wex, tx, oldCoin.exchangeBaseUrl, dph);
+ if (!denom) {
+ logger.error(`denom ${dph} not in DB`);
+ continue;
+ }
+ const car = await getCoinAvailabilityForDenom(
+ wex,
+ tx,
+ denom,
+ oldCoin.maxAge,
+ );
+ checkDbInvariant(car.pendingRefreshOutputCount != null);
+ car.pendingRefreshOutputCount =
+ car.pendingRefreshOutputCount - refreshSession.newDenoms[i].count;
+ await tx.coinAvailability.put(car);
+ }
}
function getRefreshRequestTimeout(rg: RefreshGroupRecord): Duration {
@@ -499,15 +550,29 @@ function getRefreshRequestTimeout(rg: RefreshGroupRecord): Duration {
});
}
+/**
+ * Run the melt step of a refresh session.
+ *
+ * If the melt step succeeds or fails permanently,
+ * the status in the refresh group is updated.
+ *
+ * When a transient error occurs, an exception is thrown.
+ */
async function refreshMelt(
wex: WalletExecutionContext,
refreshGroupId: string,
coinIndex: number,
): Promise<void> {
const ctx = new RefreshTransactionContext(wex, refreshGroupId);
- const transactionId = ctx.transactionId;
const d = await wex.db.runReadWriteTx(
- ["refreshGroups", "refreshSessions", "coins", "denominations"],
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ ],
+ },
async (tx) => {
const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
if (!refreshGroup) {
@@ -625,145 +690,34 @@ async function refreshMelt(
},
);
- if (resp.status === HttpStatusCode.NotFound) {
- const errDetails = await readUnexpectedResponseDetails(resp);
- const transitionInfo = await wex.db.runReadWriteTx(
- ["refreshGroups", "refreshSessions", "coins", "coinAvailability"],
- async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- if (rg.timestampFinished) {
- return;
- }
- if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
- return;
- }
- const oldTxState = computeRefreshTransactionState(rg);
- rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
- const refreshSession = await tx.refreshSessions.get([
- refreshGroupId,
- coinIndex,
- ]);
- if (!refreshSession) {
- throw Error(
- "db invariant failed: missing refresh session in database",
- );
- }
- refreshSession.lastError = errDetails;
- const updateRes = updateGroupStatus(rg);
- if (updateRes.final) {
- await makeCoinsVisible(wex, tx, transactionId);
- }
- await tx.refreshGroups.put(rg);
- await tx.refreshSessions.put(refreshSession);
- const newTxState = computeRefreshTransactionState(rg);
- return {
- oldTxState,
- newTxState,
- };
- },
- );
- wex.ws.notify({
- type: NotificationType.BalanceChange,
- hintTransactionId: transactionId,
- });
- notifyTransition(wex, transactionId, transitionInfo);
- return;
- }
-
- const exchangeBaseUrl = oldCoin.exchangeBaseUrl;
- const currency = Amounts.currencyOf(oldDenom.value);
-
- if (resp.status === HttpStatusCode.Gone) {
- const errDetail = await readTalerErrorResponse(resp);
- switch (errDetail.code) {
- case TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_REVOKED:
- case TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_EXPIRED: {
- logger.warn(`refresh ${transactionId} requires redenomination`);
- await fetchFreshExchange(wex, exchangeBaseUrl, {
- forceUpdate: true,
- });
- await updateWithdrawalDenoms(wex, exchangeBaseUrl);
- await wex.db.runReadWriteTx(
- ["refreshGroups", "refreshSessions", "denominations"],
- async (tx) => {
- const rg = await tx.refreshGroups.get(refreshGroupId);
- if (!rg) {
- return;
- }
- if (rg.timestampFinished) {
- return;
- }
- const rs = await tx.refreshSessions.get([
- refreshGroupId,
- coinIndex,
- ]);
- if (!rs) {
- return;
- }
- if (rs.norevealIndex !== undefined) {
- return;
- }
- const candidates = await getCandidateWithdrawalDenomsTx(
- wex,
- tx,
- exchangeBaseUrl,
- currency,
- );
- // We can just replace the existing coin selection, because melt is atomic,
- // and thus it's not possible that some denoms in the selection were already
- // withdrawn.
- const input = Amounts.parseOrThrow(rg.inputPerCoin[rs.coinIndex]);
- const newSel = selectWithdrawalDenominations(input, candidates);
- rs.amountRefreshOutput = newSel.totalCoinValue;
- rs.newDenoms = newSel.selectedDenoms.map((x) => ({
- count: x.count,
- denomPubHash: x.denomPubHash,
- }));
- await tx.refreshSessions.put(rs);
- },
- );
- break;
- }
+ switch (resp.status) {
+ case HttpStatusCode.NotFound: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltNotFound(ctx, coinIndex, errDetail);
+ return;
+ }
+ case HttpStatusCode.Gone: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltGone(ctx, coinIndex, errDetail);
+ return;
+ }
+ case HttpStatusCode.Conflict: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshMeltConflict(
+ ctx,
+ coinIndex,
+ errDetail,
+ derived,
+ oldCoin,
+ );
+ return;
+ }
+ case HttpStatusCode.Ok:
+ break;
+ default: {
+ const errDetail = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, errDetail);
}
-
- throwUnexpectedRequestError(resp, errDetail);
- }
-
- if (resp.status === HttpStatusCode.Conflict) {
- // Just log for better diagnostics here, error status
- // will be handled later.
- logger.error(
- `melt request for ${Amounts.stringify(
- derived.meltValueWithFee,
- )} failed in refresh group ${refreshGroupId} due to conflict`,
- );
-
- const historySig = await wex.cryptoApi.signCoinHistoryRequest({
- coinPriv: oldCoin.coinPriv,
- coinPub: oldCoin.coinPub,
- startOffset: 0,
- });
-
- const historyUrl = new URL(
- `coins/${oldCoin.coinPub}/history`,
- oldCoin.exchangeBaseUrl,
- );
-
- const historyResp = await wex.http.fetch(historyUrl.href, {
- method: "GET",
- headers: {
- "Taler-Coin-History-Signature": historySig.sig,
- },
- cancellationToken: wex.cancellationToken,
- });
-
- const historyJson = await historyResp.json();
- logger.info(`coin history: ${j2s(historyJson)}`);
-
- // FIXME: Before failing and re-trying, analyse response and adjust amount
}
const meltResponse = await readSuccessResponseJsonOrThrow(
@@ -776,7 +730,7 @@ async function refreshMelt(
refreshSession.norevealIndex = norevealIndex;
await wex.db.runReadWriteTx(
- ["refreshGroups", "refreshSessions"],
+ { storeNames: ["refreshGroups", "refreshSessions"] },
async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) {
@@ -798,6 +752,192 @@ async function refreshMelt(
);
}
+async function handleRefreshMeltGone(
+ ctx: RefreshTransactionContext,
+ coinIndex: number,
+ errDetails: TalerErrorDetail,
+): Promise<void> {
+ // const expiredMsg = codecForDenominationExpiredMessage().decode(errDetails);
+
+ // FIXME: Validate signature.
+
+ await ctx.wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(ctx.refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
+ return;
+ }
+ rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
+ const refreshSession = await tx.refreshSessions.get([
+ ctx.refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ throw Error("db invariant failed: missing refresh session in database");
+ }
+ refreshSession.lastError = errDetails;
+ await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
+ await tx.refreshGroups.put(rg);
+ await tx.refreshSessions.put(refreshSession);
+ },
+ );
+}
+
+async function handleRefreshMeltConflict(
+ ctx: RefreshTransactionContext,
+ coinIndex: number,
+ errDetails: TalerErrorDetail,
+ derived: DerivedRefreshSession,
+ oldCoin: CoinRecord,
+): Promise<void> {
+ // Just log for better diagnostics here, error status
+ // will be handled later.
+ logger.error(
+ `melt request for ${Amounts.stringify(
+ derived.meltValueWithFee,
+ )} failed in refresh group ${ctx.refreshGroupId} due to conflict`,
+ );
+
+ const historySig = await ctx.wex.cryptoApi.signCoinHistoryRequest({
+ coinPriv: oldCoin.coinPriv,
+ coinPub: oldCoin.coinPub,
+ startOffset: 0,
+ });
+
+ const historyUrl = new URL(
+ `coins/${oldCoin.coinPub}/history`,
+ oldCoin.exchangeBaseUrl,
+ );
+
+ const historyResp = await ctx.wex.http.fetch(historyUrl.href, {
+ method: "GET",
+ headers: {
+ "Taler-Coin-History-Signature": historySig.sig,
+ },
+ cancellationToken: ctx.wex.cancellationToken,
+ });
+
+ const historyJson = await readSuccessResponseJsonOrThrow(
+ historyResp,
+ codecForCoinHistoryResponse(),
+ );
+ logger.info(`coin history: ${j2s(historyJson)}`);
+
+ // FIXME: If response seems wrong, report to auditor (in the future!);
+
+ await ctx.wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "denominations",
+ "coins",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(ctx.refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
+ return;
+ }
+ if (Amounts.isZero(historyJson.balance)) {
+ rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
+ const refreshSession = await tx.refreshSessions.get([
+ ctx.refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ throw Error(
+ "db invariant failed: missing refresh session in database",
+ );
+ }
+ refreshSession.lastError = errDetails;
+ await tx.refreshGroups.put(rg);
+ await tx.refreshSessions.put(refreshSession);
+ } else {
+ // Try again with new denoms!
+ rg.inputPerCoin[coinIndex] = historyJson.balance;
+ const refreshSession = await tx.refreshSessions.get([
+ ctx.refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ throw Error(
+ "db invariant failed: missing refresh session in database",
+ );
+ }
+ await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
+ await tx.refreshSessions.delete([ctx.refreshGroupId, coinIndex]);
+ await initRefreshSession(ctx.wex, tx, rg, coinIndex);
+ }
+ },
+ );
+}
+
+async function handleRefreshMeltNotFound(
+ ctx: RefreshTransactionContext,
+ coinIndex: number,
+ errDetails: TalerErrorDetail,
+): Promise<void> {
+ // FIXME: Validate the exchange's error response
+ await ctx.wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(ctx.refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
+ return;
+ }
+ rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
+ const refreshSession = await tx.refreshSessions.get([
+ ctx.refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ throw Error("db invariant failed: missing refresh session in database");
+ }
+ await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
+ refreshSession.lastError = errDetails;
+ await tx.refreshGroups.put(rg);
+ await tx.refreshSessions.put(refreshSession);
+ },
+ );
+}
+
export async function assembleRefreshRevealRequest(args: {
cryptoApi: TalerCryptoInterface;
derived: DerivedRefreshSession;
@@ -864,8 +1004,16 @@ async function refreshReveal(
logger.trace(
`doing refresh reveal for ${refreshGroupId} (old coin ${coinIndex})`,
);
+ const ctx = new RefreshTransactionContext(wex, refreshGroupId);
const d = await wex.db.runReadOnlyTx(
- ["refreshGroups", "refreshSessions", "coins", "denominations"],
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ ],
+ },
async (tx) => {
const refreshGroup = await tx.refreshGroups.get(refreshGroupId);
if (!refreshGroup) {
@@ -991,6 +1139,21 @@ async function refreshReveal(
},
);
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ break;
+ case HttpStatusCode.Conflict:
+ case HttpStatusCode.Gone: {
+ const errDetail = await readTalerErrorResponse(resp);
+ await handleRefreshRevealError(ctx, coinIndex, errDetail);
+ return;
+ }
+ default: {
+ const errDetail = await readTalerErrorResponse(resp);
+ throwUnexpectedRequestError(resp, errDetail);
+ }
+ }
+
const reveal = await readSuccessResponseJsonOrThrow(
resp,
codecForExchangeRevealResponse(),
@@ -1043,40 +1206,105 @@ async function refreshReveal(
}
}
- const transitionInfo = await wex.db.runReadWriteTx(
- [
- "coins",
- "denominations",
- "coinAvailability",
- "refreshGroups",
- "refreshSessions",
- ],
+ await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "coins",
+ "denominations",
+ "coinAvailability",
+ "refreshGroups",
+ "refreshSessions",
+ ],
+ },
async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
if (!rg) {
logger.warn("no refresh session found");
return;
}
+ if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
+ return;
+ }
const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]);
if (!rs) {
return;
}
- const oldTxState = computeRefreshTransactionState(rg);
rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
- updateGroupStatus(rg);
for (const coin of coins) {
- await makeCoinAvailable(wex, tx, coin);
+ const existingCoin = await tx.coins.get(coin.coinPub);
+ if (existingCoin) {
+ continue;
+ }
+ await tx.coins.add(coin);
+ const denomInfo = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ checkDbInvariant(!!denomInfo);
+ const car = await getCoinAvailabilityForDenom(
+ wex,
+ tx,
+ denomInfo,
+ coin.maxAge,
+ );
+ checkDbInvariant(
+ car.pendingRefreshOutputCount != null &&
+ car.pendingRefreshOutputCount > 0,
+ );
+ car.pendingRefreshOutputCount--;
+ car.freshCoinCount++;
+ await tx.coinAvailability.put(car);
}
- await makeCoinsVisible(wex, tx, transactionId);
await tx.refreshGroups.put(rg);
- const newTxState = computeRefreshTransactionState(rg);
- return { oldTxState, newTxState };
},
);
- notifyTransition(wex, transactionId, transitionInfo);
logger.trace("refresh finished (end of reveal)");
}
+async function handleRefreshRevealError(
+ ctx: RefreshTransactionContext,
+ coinIndex: number,
+ errDetails: TalerErrorDetail,
+): Promise<void> {
+ await ctx.wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "refreshGroups",
+ "refreshSessions",
+ "coins",
+ "denominations",
+ "coinAvailability",
+ ],
+ },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(ctx.refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.timestampFinished) {
+ return;
+ }
+ if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
+ return;
+ }
+ rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
+ const refreshSession = await tx.refreshSessions.get([
+ ctx.refreshGroupId,
+ coinIndex,
+ ]);
+ if (!refreshSession) {
+ throw Error("db invariant failed: missing refresh session in database");
+ }
+ refreshSession.lastError = errDetails;
+ await destroyRefreshSession(ctx.wex, tx, rg, refreshSession);
+ await tx.refreshGroups.put(rg);
+ await tx.refreshSessions.put(refreshSession);
+ },
+ );
+}
+
export async function processRefreshGroup(
wex: WalletExecutionContext,
refreshGroupId: string,
@@ -1084,7 +1312,7 @@ export async function processRefreshGroup(
logger.trace(`processing refresh group ${refreshGroupId}`);
const refreshGroup = await wex.db.runReadOnlyTx(
- ["refreshGroups"],
+ { storeNames: ["refreshGroups"] },
async (tx) => tx.refreshGroups.get(refreshGroupId),
);
if (!refreshGroup) {
@@ -1093,6 +1321,14 @@ export async function processRefreshGroup(
if (refreshGroup.timestampFinished) {
return TaskRunResult.finished();
}
+
+ if (
+ wex.ws.config.testing.devModeActive &&
+ wex.ws.devExperimentState.blockRefreshes
+ ) {
+ throw Error("refresh blocked");
+ }
+
// Process refresh sessions of the group in parallel.
logger.trace(
`processing refresh sessions for ${refreshGroup.oldCoinPubs.length} old coins`,
@@ -1121,17 +1357,68 @@ export async function processRefreshGroup(
errors.push(getErrorDetailFromException(x));
}),
);
- try {
- logger.info("waiting for refreshes");
- await Promise.all(ps);
- logger.info("refresh group finished");
- } catch (e) {
- logger.warn("process refresh sessions got exception");
- logger.warn(`exception: ${e}`);
- }
+ await Promise.all(ps);
if (inShutdown) {
- return TaskRunResult.backoff();
+ return TaskRunResult.finished();
}
+
+ const ctx = new RefreshTransactionContext(wex, refreshGroupId);
+
+ // We've processed all refresh session and can now update the
+ // status of the whole refresh group.
+
+ const transitionInfo = await wex.db.runReadWriteTx(
+ { storeNames: ["coins", "coinAvailability", "refreshGroups"] },
+ async (tx) => {
+ const rg = await tx.refreshGroups.get(refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ switch (rg.operationStatus) {
+ case RefreshOperationStatus.Pending:
+ break;
+ default:
+ return undefined;
+ }
+ const oldTxState = computeRefreshTransactionState(rg);
+ const allFinal = fnutil.all(
+ rg.statusPerCoin,
+ (x) =>
+ x === RefreshCoinStatus.Finished || x === RefreshCoinStatus.Failed,
+ );
+ const anyFailed = fnutil.any(
+ rg.statusPerCoin,
+ (x) => x === RefreshCoinStatus.Failed,
+ );
+ if (allFinal) {
+ if (anyFailed) {
+ rg.timestampFinished = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ rg.operationStatus = RefreshOperationStatus.Failed;
+ } else {
+ rg.timestampFinished = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ rg.operationStatus = RefreshOperationStatus.Finished;
+ }
+ await makeCoinsVisible(wex, tx, ctx.transactionId);
+ await tx.refreshGroups.put(rg);
+ const newTxState = computeRefreshTransactionState(rg);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ return undefined;
+ },
+ );
+
+ if (transitionInfo) {
+ notifyTransition(wex, ctx.transactionId, transitionInfo);
+ return TaskRunResult.progress();
+ }
+
if (errors.length > 0) {
return {
type: TaskRunResultType.Error,
@@ -1157,7 +1444,7 @@ async function processRefreshSession(
`processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`,
);
let { refreshGroup, refreshSession } = await wex.db.runReadOnlyTx(
- ["refreshGroups", "refreshSessions"],
+ { storeNames: ["refreshGroups", "refreshSessions"] },
async (tx) => {
const rg = await tx.refreshGroups.get(refreshGroupId);
const rs = await tx.refreshSessions.get([refreshGroupId, coinIndex]);
@@ -1174,16 +1461,7 @@ async function processRefreshSession(
return;
}
if (!refreshSession) {
- refreshSession = await provideRefreshSession(
- wex,
- refreshGroupId,
- coinIndex,
- );
- }
- if (!refreshSession) {
- // We tried to create the refresh session, but didn't get a result back.
- // This means that either the session is finished, or that creating
- // one isn't necessary.
+ // No refresh session for that coin.
return;
}
if (refreshSession.norevealIndex === undefined) {
@@ -1211,23 +1489,6 @@ export async function calculateRefreshOutput(
const infoPerExchange: Record<string, RefreshGroupPerExchangeInfo> = {};
- // FIXME: Use denom groups instead of querying all denominations!
- const getDenoms = async (
- exchangeBaseUrl: string,
- ): Promise<DenominationRecord[]> => {
- if (denomsPerExchange[exchangeBaseUrl]) {
- return denomsPerExchange[exchangeBaseUrl];
- }
- const allDenoms = await getCandidateWithdrawalDenomsTx(
- wex,
- tx,
- exchangeBaseUrl,
- currency,
- );
- denomsPerExchange[exchangeBaseUrl] = allDenoms;
- return allDenoms;
- };
-
for (const ocp of oldCoinPubs) {
const coin = await tx.coins.get(ocp.coinPub);
checkDbInvariant(!!coin, "coin must be in database");
@@ -1242,12 +1503,11 @@ export async function calculateRefreshOutput(
"denomination for existing coin must be in database",
);
const refreshAmount = ocp.amount;
- const denoms = await getDenoms(coin.exchangeBaseUrl);
- const cost = getTotalRefreshCost(
- denoms,
+ const cost = await getTotalRefreshCost(
+ wex,
+ tx,
denom,
Amounts.parseOrThrow(refreshAmount),
- wex.ws.config.testing.denomselAllowLate,
);
const output = Amounts.sub(refreshAmount, cost).amount;
let exchInfo = infoPerExchange[coin.exchangeBaseUrl];
@@ -1268,7 +1528,7 @@ export async function calculateRefreshOutput(
};
}
-async function applyRefresh(
+async function applyRefreshToOldCoins(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
["denominations", "coins", "refreshGroups", "coinAvailability"]
@@ -1311,6 +1571,8 @@ async function applyRefresh(
coin.status = CoinStatus.Dormant;
break;
}
+ case CoinStatus.DenomLoss:
+ break;
default:
assertUnreachable(coin.status);
}
@@ -1345,20 +1607,29 @@ export interface CreateRefreshGroupResult {
export async function createRefreshGroup(
wex: WalletExecutionContext,
tx: WalletDbReadWriteTransaction<
- ["denominations", "coins", "refreshGroups", "coinAvailability"]
+ [
+ "denominations",
+ "coins",
+ "refreshGroups",
+ "refreshSessions",
+ "coinAvailability",
+ ]
>,
currency: string,
oldCoinPubs: CoinRefreshRequest[],
refreshReason: RefreshReason,
originatingTransactionId: string | undefined,
): Promise<CreateRefreshGroupResult> {
+ // FIXME: Check that involved exchanges are reasonably up-to-date.
+ // Otherwise, error out.
+
const refreshGroupId = encodeCrock(getRandomBytes(32));
const outInfo = await calculateRefreshOutput(wex, tx, currency, oldCoinPubs);
const estimatedOutputPerCoin = outInfo.outputPerCoin;
- await applyRefresh(wex, tx, oldCoinPubs, refreshGroupId);
+ await applyRefreshToOldCoins(wex, tx, oldCoinPubs, refreshGroupId);
const refreshGroup: RefreshGroupRecord = {
operationStatus: RefreshOperationStatus.Pending,
@@ -1385,6 +1656,10 @@ export async function createRefreshGroup(
refreshGroup.operationStatus = RefreshOperationStatus.Finished;
}
+ for (let i = 0; i < oldCoinPubs.length; i++) {
+ await initRefreshSession(wex, tx, refreshGroup, i);
+ }
+
await tx.refreshGroups.put(refreshGroup);
const newTxState = computeRefreshTransactionState(refreshGroup);
@@ -1459,7 +1734,7 @@ export function getRefreshesForTransaction(
wex: WalletExecutionContext,
transactionId: string,
): Promise<string[]> {
- return wex.db.runReadOnlyTx(["refreshGroups"], async (tx) => {
+ return wex.db.runReadOnlyTx({ storeNames: ["refreshGroups"] }, async (tx) => {
const groups =
await tx.refreshGroups.indexes.byOriginatingTransactionId.getAll(
transactionId,
@@ -1481,15 +1756,23 @@ export async function forceRefresh(
wex: WalletExecutionContext,
req: ForceRefreshRequest,
): Promise<ForceRefreshResult> {
- if (req.coinPubList.length == 0) {
+ if (req.refreshCoinSpecs.length == 0) {
throw Error("refusing to create empty refresh group");
}
const res = await wex.db.runReadWriteTx(
- ["refreshGroups", "coinAvailability", "denominations", "coins"],
+ {
+ storeNames: [
+ "refreshGroups",
+ "coinAvailability",
+ "refreshSessions",
+ "denominations",
+ "coins",
+ ],
+ },
async (tx) => {
let coinPubs: CoinRefreshRequest[] = [];
- for (const c of req.coinPubList) {
- const coin = await tx.coins.get(c);
+ for (const c of req.refreshCoinSpecs) {
+ const coin = await tx.coins.get(c.coinPub);
if (!coin) {
throw Error(`coin (pubkey ${c}) not found`);
}
@@ -1501,8 +1784,8 @@ export async function forceRefresh(
);
checkDbInvariant(!!denom);
coinPubs.push({
- coinPub: c,
- amount: denom?.value,
+ coinPub: c.coinPub,
+ amount: c.amount ?? denom.value,
});
}
return await createRefreshGroup(
@@ -1571,7 +1854,7 @@ async function internalWaitRefreshFinal(
// Check if refresh is final
const res = await ctx.wex.db.runReadOnlyTx(
- ["refreshGroups", "operationRetries"],
+ { storeNames: ["refreshGroups", "operationRetries"] },
async (tx) => {
return {
rg: await tx.refreshGroups.get(ctx.refreshGroupId),
diff --git a/packages/taler-wallet-core/src/reward.ts b/packages/taler-wallet-core/src/reward.ts
deleted file mode 100644
index 85e8c6606..000000000
--- a/packages/taler-wallet-core/src/reward.ts
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- AcceptTipResponse,
- Logger,
- PrepareTipResult,
- TransactionAction,
- TransactionIdStr,
- TransactionMajorState,
- TransactionMinorState,
- TransactionState,
- TransactionType,
- assertUnreachable,
-} from "@gnu-taler/taler-util";
-import {
- PendingTaskType,
- TaskIdStr,
- TaskRunResult,
- TombstoneTag,
- TransactionContext,
- constructTaskIdentifier,
-} from "./common.js";
-import { RewardRecord, RewardRecordStatus } from "./db.js";
-import {
- constructTransactionIdentifier,
-} from "./transactions.js";
-import { InternalWalletState, WalletExecutionContext } from "./wallet.js";
-
-export class RewardTransactionContext implements TransactionContext {
- public transactionId: TransactionIdStr;
- public taskId: TaskIdStr;
-
- constructor(
- public wex: WalletExecutionContext,
- public walletRewardId: string,
- ) {
- this.transactionId = constructTransactionIdentifier({
- tag: TransactionType.Reward,
- walletRewardId,
- });
- this.taskId = constructTaskIdentifier({
- tag: PendingTaskType.RewardPickup,
- walletRewardId,
- });
- }
-
- async deleteTransaction(): Promise<void> {
- throw Error("unsupported operation");
- }
-
- async suspendTransaction(): Promise<void> {
- throw Error("unsupported operation");
- }
-
- async abortTransaction(): Promise<void> {
- throw Error("unsupported operation");
- }
-
- async resumeTransaction(): Promise<void> {
- throw Error("unsupported operation");
- }
-
- async failTransaction(): Promise<void> {
- throw Error("unsupported operation");
- }
-}
-
-/**
- * Get the (DD37-style) transaction status based on the
- * database record of a reward.
- */
-export function computeRewardTransactionStatus(
- tipRecord: RewardRecord,
-): TransactionState {
- switch (tipRecord.status) {
- case RewardRecordStatus.Done:
- return {
- major: TransactionMajorState.Done,
- };
- case RewardRecordStatus.Aborted:
- return {
- major: TransactionMajorState.Aborted,
- };
- case RewardRecordStatus.PendingPickup:
- return {
- major: TransactionMajorState.Pending,
- minor: TransactionMinorState.Pickup,
- };
- case RewardRecordStatus.DialogAccept:
- return {
- major: TransactionMajorState.Dialog,
- minor: TransactionMinorState.Proposed,
- };
- case RewardRecordStatus.SuspendedPickup:
- return {
- major: TransactionMajorState.Pending,
- minor: TransactionMinorState.Pickup,
- };
- case RewardRecordStatus.Failed:
- return {
- major: TransactionMajorState.Failed,
- };
- default:
- assertUnreachable(tipRecord.status);
- }
-}
-
-export function computeTipTransactionActions(
- tipRecord: RewardRecord,
-): TransactionAction[] {
- switch (tipRecord.status) {
- case RewardRecordStatus.Done:
- return [TransactionAction.Delete];
- case RewardRecordStatus.Failed:
- return [TransactionAction.Delete];
- case RewardRecordStatus.Aborted:
- return [TransactionAction.Delete];
- case RewardRecordStatus.PendingPickup:
- return [TransactionAction.Suspend, TransactionAction.Fail];
- case RewardRecordStatus.SuspendedPickup:
- return [TransactionAction.Resume, TransactionAction.Fail];
- case RewardRecordStatus.DialogAccept:
- return [TransactionAction.Abort];
- default:
- assertUnreachable(tipRecord.status);
- }
-}
-
-export async function prepareReward(
- ws: InternalWalletState,
- talerTipUri: string,
-): Promise<PrepareTipResult> {
- throw Error("the rewards feature is not supported anymore");
-}
-
-export async function processTip(
- ws: InternalWalletState,
- walletTipId: string,
-): Promise<TaskRunResult> {
- return TaskRunResult.finished();
-}
-
-export async function acceptTip(
- ws: InternalWalletState,
- transactionId: TransactionIdStr,
-): Promise<AcceptTipResponse> {
- throw Error("the rewards feature is not supported anymore");
-}
diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts
index f04bcd2c2..3b160d97f 100644
--- a/packages/taler-wallet-core/src/shepherd.ts
+++ b/packages/taler-wallet-core/src/shepherd.ts
@@ -27,7 +27,6 @@ import {
NotificationType,
ObservabilityContext,
ObservabilityEventType,
- RetryLoopOpts,
TalerErrorDetail,
TaskThrottler,
TransactionIdStr,
@@ -37,6 +36,7 @@ import {
assertUnreachable,
getErrorDetailFromException,
j2s,
+ safeStringifyException,
} from "@gnu-taler/taler-util";
import { processBackupForProvider } from "./backup/index.js";
import {
@@ -61,7 +61,10 @@ import {
computeDepositTransactionStatus,
processDepositGroup,
} from "./deposits.js";
-import { updateExchangeFromUrlHandler } from "./exchanges.js";
+import {
+ computeDenomLossTransactionStatus,
+ updateExchangeFromUrlHandler,
+} from "./exchanges.js";
import {
computePayMerchantTransactionState,
computeRefundTransactionState,
@@ -88,7 +91,6 @@ import {
computeRefreshTransactionState,
processRefreshGroup,
} from "./refresh.js";
-import { computeRewardTransactionStatus } from "./reward.js";
import {
constructTransactionIdentifier,
parseTransactionIdentifier,
@@ -140,13 +142,14 @@ function taskGivesLiveness(taskId: string): boolean {
}
export interface TaskScheduler {
- ensureRunning(): void;
- run(opts?: RetryLoopOpts): Promise<void>;
+ ensureRunning(): Promise<void>;
startShepherdTask(taskId: TaskIdStr): void;
stopShepherdTask(taskId: TaskIdStr): void;
resetTaskRetries(taskId: TaskIdStr): Promise<void>;
- reload(): void;
+ reload(): Promise<void>;
getActiveTasks(): TaskIdStr[];
+ isIdle(): boolean;
+ shutdown(): Promise<void>;
}
export class TaskSchedulerImpl implements TaskScheduler {
@@ -174,58 +177,73 @@ export class TaskSchedulerImpl implements TaskScheduler {
return [...this.sheps.keys()];
}
- ensureRunning(): void {
+ async shutdown(): Promise<void> {
+ const tasksIds = [...this.sheps.keys()];
+ logger.info(`Stopping task shepherd.`);
+ for (const taskId of tasksIds) {
+ this.stopShepherdTask(taskId);
+ }
+ }
+
+ async ensureRunning(): Promise<void> {
if (this.isRunning) {
return;
}
+ this.isRunning = true;
+ try {
+ await this.loadTasksFromDb();
+ } catch (e) {
+ this.isRunning = false;
+ throw e;
+ }
this.run()
.catch((e) => {
logger.error("error running task loop");
logger.error(`err: ${e}`);
})
.then(() => {
- logger.info("done running task loop");
+ logger.trace("done running task loop");
+ this.isRunning = false;
});
}
- async run(opts: RetryLoopOpts = {}): Promise<void> {
- if (this.isRunning) {
- throw Error("task loop already running");
+ isIdle(): boolean {
+ let alive = false;
+ const taskIds = [...this.sheps.keys()];
+ for (const taskId of taskIds) {
+ if (taskGivesLiveness(taskId)) {
+ alive = true;
+ break;
+ }
}
- logger.info("Running task loop.");
- this.isRunning = true;
- await this.loadTasksFromDb();
- logger.info("loaded!");
- logger.info(`sheps: ${this.sheps.size}`);
+ // We're idle if no task is alive anymore.
+ return !alive;
+ }
+
+ private async run(): Promise<void> {
+ logger.trace("Running task loop.");
+ logger.trace(`sheps: ${this.sheps.size}`);
while (true) {
- if (opts.stopWhenDone) {
- let alive = false;
- const taskIds = [...this.sheps.keys()];
- logger.info(`current task IDs: ${j2s(taskIds)}`);
- logger.info(`sheps: ${this.sheps.size}`);
- for (const taskId of taskIds) {
- if (taskGivesLiveness(taskId)) {
- alive = true;
- break;
- }
- }
- if (!alive) {
- logger.info("Breaking out of task loop (no more work).");
- break;
- }
- }
if (this.ws.stopped) {
- logger.info("Breaking out of task loop (wallet stopped).");
+ logger.trace("Breaking out of task loop (wallet stopped).");
break;
}
+
+ if (this.isIdle()) {
+ this.ws.notify({
+ type: NotificationType.Idle,
+ });
+ }
+
await this.iterCond.wait();
}
- this.isRunning = false;
- logger.info("Done with task loop.");
+ logger.trace("Done with task loop.");
}
startShepherdTask(taskId: TaskIdStr): void {
- this.ensureRunning();
+ this.ensureRunning().catch((e) => {
+ logger.error(`error running scheduler: ${safeStringifyException(e)}`);
+ });
// Run in the background, no await!
this.internalStartShepherdTask(taskId);
}
@@ -235,8 +253,8 @@ export class TaskSchedulerImpl implements TaskScheduler {
*
* Mostly useful to interrupt all waits when time-travelling.
*/
- reload() {
- this.ensureRunning();
+ async reload(): Promise<void> {
+ await this.ensureRunning();
const tasksIds = [...this.sheps.keys()];
logger.info(`reloading sheperd with ${tasksIds.length} tasks`);
for (const taskId of tasksIds) {
@@ -351,11 +369,11 @@ export class TaskSchedulerImpl implements TaskScheduler {
};
}
if (info.cts.token.isCancelled) {
- logger.info("task cancelled, not processing result");
+ logger.trace("task cancelled, not processing result");
return;
}
if (this.ws.stopped) {
- logger.info("wallet stopped, not processing result");
+ logger.trace("wallet stopped, not processing result");
return;
}
wex.oc.observe({
@@ -370,9 +388,8 @@ export class TaskSchedulerImpl implements TaskScheduler {
taskId,
res.errorDetail,
);
- let delay: Duration;
const t = timestampAbsoluteFromDb(retryRecord.retryInfo.nextRetry);
- delay = AbsoluteTime.remaining(t);
+ const delay = AbsoluteTime.remaining(t);
logger.trace(`Waiting for ${delay.d_ms} ms`);
await this.wait(taskId, info, delay);
break;
@@ -380,9 +397,8 @@ export class TaskSchedulerImpl implements TaskScheduler {
case TaskRunResultType.Backoff: {
logger.trace(`Shepherd for ${taskId} got backoff result.`);
const retryRecord = await storePendingTaskPending(this.ws, taskId);
- let delay: Duration;
const t = timestampAbsoluteFromDb(retryRecord.retryInfo.nextRetry);
- delay = AbsoluteTime.remaining(t);
+ const delay = AbsoluteTime.remaining(t);
logger.trace(`Waiting for ${delay.d_ms} ms`);
await this.wait(taskId, info, delay);
break;
@@ -394,13 +410,14 @@ export class TaskSchedulerImpl implements TaskScheduler {
await storeTaskProgress(this.ws, taskId);
break;
}
- case TaskRunResultType.ScheduleLater:
+ case TaskRunResultType.ScheduleLater: {
logger.trace(`Shepherd for ${taskId} got schedule-later result.`);
await storeTaskProgress(this.ws, taskId);
const delay = AbsoluteTime.remaining(res.runAt);
logger.trace(`Waiting for ${delay.d_ms} ms`);
await this.wait(taskId, info, delay);
break;
+ }
case TaskRunResultType.Finished:
logger.trace(`Shepherd for ${taskId} got finished result.`);
await storePendingTaskFinished(this.ws, taskId);
@@ -466,9 +483,12 @@ async function storeTaskProgress(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
- await ws.db.runReadWriteTx(["operationRetries"], async (tx) => {
- await tx.operationRetries.delete(pendingTaskId);
- });
+ await ws.db.runReadWriteTx(
+ { storeNames: ["operationRetries"] },
+ async (tx) => {
+ await tx.operationRetries.delete(pendingTaskId);
+ },
+ );
}
async function storePendingTaskPending(
@@ -515,9 +535,12 @@ async function storePendingTaskFinished(
ws: InternalWalletState,
pendingTaskId: string,
): Promise<void> {
- await ws.db.runReadWriteTx(["operationRetries"], async (tx) => {
- await tx.operationRetries.delete(pendingTaskId);
- });
+ await ws.db.runReadWriteTx(
+ { storeNames: ["operationRetries"] },
+ async (tx) => {
+ await tx.operationRetries.delete(pendingTaskId);
+ },
+ );
}
function getWalletExecutionContextForTask(
@@ -636,6 +659,7 @@ async function getTransactionState(
"peerPushCredit",
"rewards",
"refreshGroups",
+ "denomLossEvents",
]
>,
transactionId: string,
@@ -674,12 +698,13 @@ async function getTransactionState(
}
return computeRefundTransactionState(rec);
}
- case TransactionType.PeerPullCredit:
+ case TransactionType.PeerPullCredit: {
const rec = await tx.peerPullCredit.get(parsedTxId.pursePub);
if (!rec) {
return undefined;
}
return computePeerPullCreditTransactionState(rec);
+ }
case TransactionType.PeerPullDebit: {
const rec = await tx.peerPullDebit.get(parsedTxId.peerPullDebitId);
if (!rec) {
@@ -708,15 +733,15 @@ async function getTransactionState(
}
return computeRefreshTransactionState(rec);
}
- case TransactionType.Reward: {
- const rec = await tx.rewards.get(parsedTxId.walletRewardId);
+ case TransactionType.Recoup:
+ throw Error("not yet supported");
+ case TransactionType.DenomLoss: {
+ const rec = await tx.denomLossEvents.get(parsedTxId.denomLossEventId);
if (!rec) {
return undefined;
}
- return computeRewardTransactionStatus(rec);
+ return computeDenomLossTransactionStatus(rec);
}
- case TransactionType.Recoup:
- throw Error("not yet supported");
default:
assertUnreachable(parsedTxId);
}
@@ -855,8 +880,6 @@ export function listTaskForTransactionId(transactionId: string): TaskIdStr[] {
];
case TransactionType.Refund:
return [];
- case TransactionType.Reward:
- return [];
case TransactionType.Withdrawal:
return [
constructTaskIdentifier({
@@ -864,6 +887,8 @@ export function listTaskForTransactionId(transactionId: string): TaskIdStr[] {
withdrawalGroupId: tid.withdrawalGroupId,
}),
];
+ case TransactionType.DenomLoss:
+ return [];
default:
assertUnreachable(tid);
}
@@ -911,11 +936,6 @@ export function convertTaskToTransactionId(
tag: TransactionType.Refresh,
refreshGroupId: parsedTaskId.refreshGroupId,
});
- case PendingTaskType.RewardPickup:
- return constructTransactionIdentifier({
- tag: TransactionType.Reward,
- walletRewardId: parsedTaskId.walletRewardId,
- });
case PendingTaskType.PeerPushDebit:
return constructTransactionIdentifier({
tag: TransactionType.PeerPushDebit,
@@ -942,18 +962,20 @@ export async function getActiveTaskIds(
taskIds: [],
};
await ws.db.runReadWriteTx(
- [
- "exchanges",
- "refreshGroups",
- "withdrawalGroups",
- "purchases",
- "depositGroups",
- "recoupGroups",
- "peerPullCredit",
- "peerPushDebit",
- "peerPullDebit",
- "peerPushCredit",
- ],
+ {
+ storeNames: [
+ "exchanges",
+ "refreshGroups",
+ "withdrawalGroups",
+ "purchases",
+ "depositGroups",
+ "recoupGroups",
+ "peerPullCredit",
+ "peerPushDebit",
+ "peerPullDebit",
+ "peerPushCredit",
+ ],
+ },
async (tx) => {
const active = GlobalIDB.KeyRange.bound(
OPERATION_STATUS_ACTIVE_FIRST,
diff --git a/packages/taler-wallet-core/src/testing.ts b/packages/taler-wallet-core/src/testing.ts
index b192e7b70..899c4a8b2 100644
--- a/packages/taler-wallet-core/src/testing.ts
+++ b/packages/taler-wallet-core/src/testing.ts
@@ -39,8 +39,6 @@ import {
j2s,
Logger,
NotificationType,
- OpenedPromise,
- openPromise,
parsePaytoUri,
PreparePayResultType,
TalerCorebankApiClient,
@@ -58,6 +56,7 @@ import {
readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http";
import { getBalances } from "./balance.js";
+import { genericWaitForState } from "./common.js";
import { createDepositGroup } from "./deposits.js";
import { fetchFreshExchange } from "./exchanges.js";
import {
@@ -402,52 +401,56 @@ export async function waitUntilAllTransactionsFinal(
wex: WalletExecutionContext,
): Promise<void> {
logger.info("waiting until all transactions are in a final state");
- wex.taskScheduler.ensureRunning();
- let p: OpenedPromise<void> | undefined = undefined;
- const cancelNotifs = wex.ws.addNotificationListener((notif) => {
- if (!p) {
- return;
- }
- if (notif.type === NotificationType.TransactionStateTransition) {
+ await wex.taskScheduler.ensureRunning();
+ await genericWaitForState(wex, {
+ filterNotification(notif) {
+ if (notif.type !== NotificationType.TransactionStateTransition) {
+ return false;
+ }
switch (notif.newTxState.major) {
case TransactionMajorState.Pending:
case TransactionMajorState.Aborting:
- break;
+ return false;
default:
- p.resolve();
+ return true;
}
- }
- });
- while (1) {
- p = openPromise();
- const txs = await getTransactions(wex, {
- includeRefreshes: true,
- filterByState: "nonfinal",
- });
- let finished = true;
- for (const tx of txs.transactions) {
- switch (tx.txState.major) {
- case TransactionMajorState.Pending:
- case TransactionMajorState.Aborting:
- case TransactionMajorState.Suspended:
- case TransactionMajorState.SuspendedAborting:
- finished = false;
- logger.info(
- `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
- );
- break;
+ },
+ async checkState() {
+ const txs = await getTransactions(wex, {
+ includeRefreshes: true,
+ filterByState: "nonfinal",
+ });
+ for (const tx of txs.transactions) {
+ switch (tx.txState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ case TransactionMajorState.Suspended:
+ case TransactionMajorState.SuspendedAborting:
+ logger.info(
+ `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
+ );
+ return false;
+ }
}
- }
- if (finished) {
- break;
- }
- // Wait until transaction state changed
- await p.promise;
- }
- cancelNotifs();
+ return true;
+ },
+ });
logger.info("done waiting until all transactions are in a final state");
}
+export async function waitTasksDone(
+ wex: WalletExecutionContext,
+): Promise<void> {
+ await genericWaitForState(wex, {
+ async checkState() {
+ return wex.taskScheduler.isIdle();
+ },
+ filterNotification(notif) {
+ return notif.type === NotificationType.Idle;
+ },
+ });
+}
+
/**
* Wait until all chosen transactions are in a final state.
*/
@@ -462,59 +465,51 @@ export async function waitUntilGivenTransactionsFinal(
if (transactionIds.length === 0) {
return;
}
- wex.taskScheduler.ensureRunning();
+
const txIdSet = new Set(transactionIds);
- let p: OpenedPromise<void> | undefined = undefined;
- const cancelNotifs = wex.ws.addNotificationListener((notif) => {
- if (!p) {
- return;
- }
- if (notif.type === NotificationType.TransactionStateTransition) {
+
+ await genericWaitForState(wex, {
+ filterNotification(notif) {
+ if (notif.type !== NotificationType.TransactionStateTransition) {
+ return false;
+ }
if (!txIdSet.has(notif.transactionId)) {
- return;
+ return false;
}
switch (notif.newTxState.major) {
case TransactionMajorState.Pending:
case TransactionMajorState.Aborting:
case TransactionMajorState.Suspended:
case TransactionMajorState.SuspendedAborting:
- break;
- default:
- p.resolve();
- }
- }
- });
- while (1) {
- p = openPromise();
- const txs = await getTransactions(wex, {
- includeRefreshes: true,
- filterByState: "nonfinal",
- });
- let finished = true;
- for (const tx of txs.transactions) {
- if (!txIdSet.has(tx.transactionId)) {
- // Don't look at this transaction, we're not interested in it.
- continue;
+ return false;
}
- switch (tx.txState.major) {
- case TransactionMajorState.Pending:
- case TransactionMajorState.Aborting:
- case TransactionMajorState.Suspended:
- case TransactionMajorState.SuspendedAborting:
- finished = false;
- logger.info(
- `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
- );
- break;
+ return true;
+ },
+ async checkState() {
+ const txs = await getTransactions(wex, {
+ includeRefreshes: true,
+ filterByState: "nonfinal",
+ });
+ for (const tx of txs.transactions) {
+ if (!txIdSet.has(tx.transactionId)) {
+ // Don't look at this transaction, we're not interested in it.
+ continue;
+ }
+ switch (tx.txState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ case TransactionMajorState.Suspended:
+ case TransactionMajorState.SuspendedAborting:
+ logger.info(
+ `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
+ );
+ return false;
+ }
}
- }
- if (finished) {
- break;
- }
- // Wait until transaction state changed
- await p.promise;
- }
- cancelNotifs();
+ // No transaction is pending, we're done waiting!
+ return true;
+ },
+ });
logger.info("done waiting until given transactions are in a final state");
}
@@ -522,52 +517,43 @@ export async function waitUntilRefreshesDone(
wex: WalletExecutionContext,
): Promise<void> {
logger.info("waiting until all refresh transactions are in a final state");
- wex.taskScheduler.ensureRunning();
- let p: OpenedPromise<void> | undefined = undefined;
- const cancelNotifs = wex.ws.addNotificationListener((notif) => {
- if (!p) {
- return;
- }
- if (notif.type === NotificationType.TransactionStateTransition) {
+
+ await genericWaitForState(wex, {
+ filterNotification(notif) {
+ if (notif.type !== NotificationType.TransactionStateTransition) {
+ return false;
+ }
switch (notif.newTxState.major) {
case TransactionMajorState.Pending:
case TransactionMajorState.Aborting:
- break;
+ return false;
default:
- p.resolve();
+ return true;
}
- }
- });
- while (1) {
- p = openPromise();
- const txs = await getTransactions(wex, {
- includeRefreshes: true,
- filterByState: "nonfinal",
- });
- let finished = true;
- for (const tx of txs.transactions) {
- if (tx.type !== TransactionType.Refresh) {
- continue;
- }
- switch (tx.txState.major) {
- case TransactionMajorState.Pending:
- case TransactionMajorState.Aborting:
- case TransactionMajorState.Suspended:
- case TransactionMajorState.SuspendedAborting:
- finished = false;
- logger.info(
- `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
- );
- break;
+ },
+ async checkState() {
+ const txs = await getTransactions(wex, {
+ includeRefreshes: true,
+ filterByState: "nonfinal",
+ });
+ for (const tx of txs.transactions) {
+ if (tx.type !== TransactionType.Refresh) {
+ continue;
+ }
+ switch (tx.txState.major) {
+ case TransactionMajorState.Pending:
+ case TransactionMajorState.Aborting:
+ case TransactionMajorState.Suspended:
+ case TransactionMajorState.SuspendedAborting:
+ logger.info(
+ `continuing waiting, ${tx.transactionId} in ${tx.txState.major}(${tx.txState.minor})`,
+ );
+ return false;
+ }
}
- }
- if (finished) {
- break;
- }
- // Wait until transaction state changed
- await p.promise;
- }
- cancelNotifs();
+ return true;
+ },
+ });
logger.info("done waiting until all refreshes are in a final state");
}
@@ -575,33 +561,10 @@ async function waitUntilTransactionPendingReady(
wex: WalletExecutionContext,
transactionId: string,
): Promise<void> {
- logger.info(`starting waiting for ${transactionId} to be in pending(ready)`);
- wex.taskScheduler.ensureRunning();
- let p: OpenedPromise<void> | undefined = undefined;
- const cancelNotifs = wex.ws.addNotificationListener((notif) => {
- if (!p) {
- return;
- }
- if (notif.type === NotificationType.TransactionStateTransition) {
- p.resolve();
- }
+ return await waitTransactionState(wex, transactionId, {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Ready,
});
- while (1) {
- p = openPromise();
- const tx = await getTransactionById(wex, {
- transactionId,
- });
- if (
- tx.txState.major == TransactionMajorState.Pending &&
- tx.txState.minor === TransactionMinorState.Ready
- ) {
- break;
- }
- // Wait until transaction state changed
- await p.promise;
- }
- logger.info(`done waiting for ${transactionId} to be in pending(ready)`);
- cancelNotifs();
}
/**
@@ -617,34 +580,22 @@ export async function waitTransactionState(
txState,
)})`,
);
- wex.taskScheduler.ensureRunning();
- let p: OpenedPromise<void> | undefined = undefined;
- const cancelNotifs = wex.ws.addNotificationListener((notif) => {
- if (!p) {
- return;
- }
- if (notif.type === NotificationType.TransactionStateTransition) {
- p.resolve();
- }
+ await genericWaitForState(wex, {
+ async checkState() {
+ const tx = await getTransactionById(wex, {
+ transactionId,
+ });
+ return (
+ tx.txState.major === txState.major && tx.txState.minor === txState.minor
+ );
+ },
+ filterNotification(notif) {
+ return notif.type === NotificationType.TransactionStateTransition;
+ },
});
- while (1) {
- p = openPromise();
- const tx = await getTransactionById(wex, {
- 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 waitUntilTransactionWithAssociatedRefreshesFinal(
@@ -669,7 +620,7 @@ export async function runIntegrationTest2(
wex: WalletExecutionContext,
args: IntegrationTestV2Args,
): Promise<void> {
- wex.taskScheduler.ensureRunning();
+ await wex.taskScheduler.ensureRunning();
logger.info("running test with arguments", args);
const exchangeInfo = await fetchFreshExchange(wex, args.exchangeBaseUrl);
@@ -907,11 +858,14 @@ export async function testPay(
if (r.type != ConfirmPayResultType.Done) {
throw Error("payment not done");
}
- const purchase = await wex.db.runReadOnlyTx(["purchases"], async (tx) => {
- return tx.purchases.get(result.proposalId);
- });
+ const purchase = await wex.db.runReadOnlyTx(
+ { storeNames: ["purchases"] },
+ async (tx) => {
+ return tx.purchases.get(result.proposalId);
+ },
+ );
checkLogicInvariant(!!purchase);
return {
- numCoins: purchase.payInfo?.payCoinSelection.coinContributions.length ?? 0,
+ numCoins: purchase.payInfo?.payCoinSelection?.coinContributions.length ?? 0,
};
}
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
index 7ece43dc5..f6216d641 100644
--- a/packages/taler-wallet-core/src/transactions.ts
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -23,7 +23,6 @@ import {
Amounts,
assertUnreachable,
checkDbInvariant,
- checkLogicInvariant,
DepositTransactionTrackingState,
j2s,
Logger,
@@ -38,6 +37,7 @@ import {
TalerErrorCode,
TalerPreciseTimestamp,
Transaction,
+ TransactionAction,
TransactionByIdRequest,
TransactionIdStr,
TransactionMajorState,
@@ -59,6 +59,7 @@ import {
TransactionContext,
} from "./common.js";
import {
+ DenomLossEventRecord,
DepositElementStatus,
DepositGroupRecord,
OPERATION_STATUS_ACTIVE_FIRST,
@@ -76,7 +77,6 @@ import {
RefreshGroupRecord,
RefreshOperationStatus,
RefundGroupRecord,
- RewardRecord,
timestampPreciseFromDb,
timestampProtocolFromDb,
WalletDbReadOnlyTransaction,
@@ -90,6 +90,8 @@ import {
DepositTransactionContext,
} from "./deposits.js";
import {
+ computeDenomLossTransactionStatus,
+ DenomLossTransactionContext,
ExchangeWireDetails,
getExchangeWireDetailsInTx,
} from "./exchanges.js";
@@ -127,11 +129,6 @@ import {
computeRefreshTransactionState,
RefreshTransactionContext,
} from "./refresh.js";
-import {
- computeRewardTransactionStatus,
- computeTipTransactionActions,
- RewardTransactionContext,
-} from "./reward.js";
import type { WalletExecutionContext } from "./wallet.js";
import {
augmentPaytoUrisForWithdrawal,
@@ -201,7 +198,6 @@ function shouldSkipSearch(
*/
const txOrder: { [t in TransactionType]: number } = {
[TransactionType.Withdrawal]: 1,
- [TransactionType.Reward]: 2,
[TransactionType.Payment]: 3,
[TransactionType.PeerPullCredit]: 4,
[TransactionType.PeerPullDebit]: 5,
@@ -212,6 +208,7 @@ const txOrder: { [t in TransactionType]: number } = {
[TransactionType.Refresh]: 10,
[TransactionType.Recoup]: 11,
[TransactionType.InternalWithdrawal]: 12,
+ [TransactionType.DenomLoss]: 13,
};
export async function getTransactionById(
@@ -229,12 +226,14 @@ export async function getTransactionById(
case TransactionType.Withdrawal: {
const withdrawalGroupId = parsedTx.withdrawalGroupId;
return await wex.db.runReadWriteTx(
- [
- "withdrawalGroups",
- "exchangeDetails",
- "exchanges",
- "operationRetries",
- ],
+ {
+ storeNames: [
+ "withdrawalGroups",
+ "exchangeDetails",
+ "exchanges",
+ "operationRetries",
+ ],
+ },
async (tx) => {
const withdrawalGroupRecord =
await tx.withdrawalGroups.get(withdrawalGroupId);
@@ -268,19 +267,34 @@ export async function getTransactionById(
);
}
+ case TransactionType.DenomLoss: {
+ const rec = await wex.db.runReadOnlyTx(
+ { storeNames: ["denomLossEvents"] },
+ async (tx) => {
+ return tx.denomLossEvents.get(parsedTx.denomLossEventId);
+ },
+ );
+ if (!rec) {
+ throw Error("denom loss record not found");
+ }
+ return buildTransactionForDenomLoss(rec);
+ }
+
case TransactionType.Recoup:
throw new Error("not yet supported");
case TransactionType.Payment: {
const proposalId = parsedTx.proposalId;
return await wex.db.runReadWriteTx(
- [
- "purchases",
- "tombstones",
- "operationRetries",
- "contractTerms",
- "refundGroups",
- ],
+ {
+ storeNames: [
+ "purchases",
+ "tombstones",
+ "operationRetries",
+ "contractTerms",
+ "refundGroups",
+ ],
+ },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) throw Error("not found");
@@ -307,7 +321,7 @@ export async function getTransactionById(
// FIXME: We should return info about the refresh here!;
const refreshGroupId = parsedTx.refreshGroupId;
return await wex.db.runReadOnlyTx(
- ["refreshGroups", "operationRetries"],
+ { storeNames: ["refreshGroups", "operationRetries"] },
async (tx) => {
const refreshGroupRec = await tx.refreshGroups.get(refreshGroupId);
if (!refreshGroupRec) {
@@ -321,14 +335,10 @@ export async function getTransactionById(
);
}
- case TransactionType.Reward: {
- throw Error("unsupported operation");
- }
-
case TransactionType.Deposit: {
const depositGroupId = parsedTx.depositGroupId;
return await wex.db.runReadWriteTx(
- ["depositGroups", "operationRetries"],
+ { storeNames: ["depositGroups", "operationRetries"] },
async (tx) => {
const depositRecord = await tx.depositGroups.get(depositGroupId);
if (!depositRecord) throw Error("not found");
@@ -343,7 +353,14 @@ export async function getTransactionById(
case TransactionType.Refund: {
return await wex.db.runReadOnlyTx(
- ["refundGroups", "purchases", "operationRetries", "contractTerms"],
+ {
+ storeNames: [
+ "refundGroups",
+ "purchases",
+ "operationRetries",
+ "contractTerms",
+ ],
+ },
async (tx) => {
const refundRecord = await tx.refundGroups.get(
parsedTx.refundGroupId,
@@ -361,7 +378,7 @@ export async function getTransactionById(
}
case TransactionType.PeerPullDebit: {
return await wex.db.runReadWriteTx(
- ["peerPullDebit", "contractTerms"],
+ { storeNames: ["peerPullDebit", "contractTerms"] },
async (tx) => {
const debit = await tx.peerPullDebit.get(parsedTx.peerPullDebitId);
if (!debit) throw Error("not found");
@@ -380,7 +397,7 @@ export async function getTransactionById(
case TransactionType.PeerPushDebit: {
return await wex.db.runReadWriteTx(
- ["peerPushDebit", "contractTerms"],
+ { storeNames: ["peerPushDebit", "contractTerms"] },
async (tx) => {
const debit = await tx.peerPushDebit.get(parsedTx.pursePub);
if (!debit) throw Error("not found");
@@ -397,12 +414,14 @@ export async function getTransactionById(
case TransactionType.PeerPushCredit: {
const peerPushCreditId = parsedTx.peerPushCreditId;
return await wex.db.runReadWriteTx(
- [
- "peerPushCredit",
- "contractTerms",
- "withdrawalGroups",
- "operationRetries",
- ],
+ {
+ storeNames: [
+ "peerPushCredit",
+ "contractTerms",
+ "withdrawalGroups",
+ "operationRetries",
+ ],
+ },
async (tx) => {
const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
if (!pushInc) throw Error("not found");
@@ -435,12 +454,14 @@ export async function getTransactionById(
case TransactionType.PeerPullCredit: {
const pursePub = parsedTx.pursePub;
return await wex.db.runReadWriteTx(
- [
- "peerPullCredit",
- "contractTerms",
- "withdrawalGroups",
- "operationRetries",
- ],
+ {
+ storeNames: [
+ "peerPullCredit",
+ "contractTerms",
+ "withdrawalGroups",
+ "operationRetries",
+ ],
+ },
async (tx) => {
const pushInc = await tx.peerPullCredit.get(pursePub);
if (!pushInc) throw Error("not found");
@@ -859,15 +880,37 @@ function buildTransactionForRefresh(
};
}
+function buildTransactionForDenomLoss(rec: DenomLossEventRecord): Transaction {
+ const txState = computeDenomLossTransactionStatus(rec);
+ return {
+ type: TransactionType.DenomLoss,
+ txState,
+ txActions: [TransactionAction.Delete],
+ amountRaw: Amounts.stringify(rec.amount),
+ amountEffective: Amounts.stringify(rec.amount),
+ timestamp: timestampPreciseFromDb(rec.timestampCreated),
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.DenomLoss,
+ denomLossEventId: rec.denomLossEventId,
+ }),
+ lossEventType: rec.eventType,
+ exchangeBaseUrl: rec.exchangeBaseUrl,
+ };
+}
+
function buildTransactionForDeposit(
dg: DepositGroupRecord,
ort?: OperationRetryRecord,
): Transaction {
let deposited = true;
- for (const d of dg.statusPerCoin) {
- if (d == DepositElementStatus.DepositPending) {
- deposited = false;
+ if (dg.statusPerCoin) {
+ for (const d of dg.statusPerCoin) {
+ if (d == DepositElementStatus.DepositPending) {
+ deposited = false;
+ }
}
+ } else {
+ deposited = false;
}
const trackingState: DepositTransactionTrackingState[] = [];
@@ -881,6 +924,17 @@ function buildTransactionForDeposit(
});
}
+ let wireTransferProgress = 0;
+ if (dg.statusPerCoin) {
+ wireTransferProgress =
+ (100 *
+ dg.statusPerCoin.reduce(
+ (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0),
+ 0,
+ )) /
+ dg.statusPerCoin.length;
+ }
+
const txState = computeDepositTransactionStatus(dg);
return {
type: TransactionType.Deposit,
@@ -897,13 +951,7 @@ function buildTransactionForDeposit(
tag: TransactionType.Deposit,
depositGroupId: dg.depositGroupId,
}),
- wireTransferProgress:
- (100 *
- dg.statusPerCoin.reduce(
- (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0),
- 0,
- )) /
- dg.statusPerCoin.length,
+ wireTransferProgress,
depositGroupId: dg.depositGroupId,
trackingState,
deposited,
@@ -1006,7 +1054,14 @@ export async function getWithdrawalTransactionByUri(
request: WithdrawalTransactionByURIRequest,
): Promise<TransactionWithdrawal | undefined> {
return await wex.db.runReadWriteTx(
- ["withdrawalGroups", "exchangeDetails", "exchanges", "operationRetries"],
+ {
+ storeNames: [
+ "withdrawalGroups",
+ "exchangeDetails",
+ "exchanges",
+ "operationRetries",
+ ],
+ },
async (tx) => {
const withdrawalGroupRecord =
await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
@@ -1059,27 +1114,30 @@ export async function getTransactions(
}
await wex.db.runReadOnlyTx(
- [
- "coins",
- "denominations",
- "depositGroups",
- "exchangeDetails",
- "exchanges",
- "operationRetries",
- "peerPullDebit",
- "peerPushDebit",
- "peerPushCredit",
- "peerPullCredit",
- "planchets",
- "purchases",
- "contractTerms",
- "recoupGroups",
- "rewards",
- "tombstones",
- "withdrawalGroups",
- "refreshGroups",
- "refundGroups",
- ],
+ {
+ storeNames: [
+ "coins",
+ "denominations",
+ "depositGroups",
+ "exchangeDetails",
+ "exchanges",
+ "operationRetries",
+ "peerPullDebit",
+ "peerPushDebit",
+ "peerPushCredit",
+ "peerPullCredit",
+ "planchets",
+ "purchases",
+ "contractTerms",
+ "recoupGroups",
+ "rewards",
+ "tombstones",
+ "withdrawalGroups",
+ "refreshGroups",
+ "refundGroups",
+ "denomLossEvents",
+ ],
+ },
async (tx) => {
await iterRecordsForPeerPushDebit(tx, filter, async (pi) => {
const amount = Amounts.parseOrThrow(pi.amount);
@@ -1224,7 +1282,10 @@ export async function getTransactions(
const exchangesInTx: string[] = [];
const p = await tx.purchases.get(refundGroup.proposalId);
- if (!p || !p.payInfo) return; //refund with no payment
+ if (!p || !p.payInfo || !p.payInfo.payCoinSelection) {
+ //refund with no payment
+ return;
+ }
// FIXME: This is very slow, should become obsolete with materialized transactions.
for (const cp of p.payInfo.payCoinSelection.coinPubs) {
@@ -1325,6 +1386,21 @@ export async function getTransactions(
}
});
+ await iterRecordsForDenomLoss(tx, filter, async (rec) => {
+ const amount = Amounts.parseOrThrow(rec.amount);
+ const exchangesInTx = [rec.exchangeBaseUrl];
+ if (
+ shouldSkipCurrency(
+ transactionsRequest,
+ amount.currency,
+ exchangesInTx,
+ )
+ ) {
+ return;
+ }
+ transactions.push(buildTransactionForDenomLoss(rec));
+ });
+
await iterRecordsForDeposit(tx, filter, async (dg) => {
const amount = Amounts.parseOrThrow(dg.amount);
const exchangesInTx = dg.infoPerExchange
@@ -1355,7 +1431,7 @@ export async function getTransactions(
}
const exchangesInTx: string[] = [];
- for (const cp of purchase.payInfo.payCoinSelection.coinPubs) {
+ for (const cp of purchase.payInfo.payCoinSelection?.coinPubs ?? []) {
const c = await tx.coins.get(cp);
if (c?.exchangeBaseUrl) {
exchangesInTx.push(c.exchangeBaseUrl);
@@ -1473,10 +1549,10 @@ export type ParsedTransactionIdentifier =
| { tag: TransactionType.PeerPushDebit; pursePub: string }
| { tag: TransactionType.Refresh; refreshGroupId: string }
| { tag: TransactionType.Refund; refundGroupId: string }
- | { tag: TransactionType.Reward; walletRewardId: string }
| { tag: TransactionType.Withdrawal; withdrawalGroupId: string }
| { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string }
- | { tag: TransactionType.Recoup; recoupGroupId: string };
+ | { tag: TransactionType.Recoup; recoupGroupId: string }
+ | { tag: TransactionType.DenomLoss; denomLossEventId: string };
export function constructTransactionIdentifier(
pTxId: ParsedTransactionIdentifier,
@@ -1498,14 +1574,14 @@ export function constructTransactionIdentifier(
return `txn:${pTxId.tag}:${pTxId.refreshGroupId}` as TransactionIdStr;
case TransactionType.Refund:
return `txn:${pTxId.tag}:${pTxId.refundGroupId}` as TransactionIdStr;
- case TransactionType.Reward:
- return `txn:${pTxId.tag}:${pTxId.walletRewardId}` as TransactionIdStr;
case TransactionType.Withdrawal:
return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr;
case TransactionType.InternalWithdrawal:
return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr;
case TransactionType.Recoup:
return `txn:${pTxId.tag}:${pTxId.recoupGroupId}` as TransactionIdStr;
+ case TransactionType.DenomLoss:
+ return `txn:${pTxId.tag}:${pTxId.denomLossEventId}` as TransactionIdStr;
default:
assertUnreachable(pTxId);
}
@@ -1555,16 +1631,16 @@ export function parseTransactionIdentifier(
tag: TransactionType.Refund,
refundGroupId: rest[0],
};
- case TransactionType.Reward:
- return {
- tag: TransactionType.Reward,
- walletRewardId: rest[0],
- };
case TransactionType.Withdrawal:
return {
tag: TransactionType.Withdrawal,
withdrawalGroupId: rest[0],
};
+ case TransactionType.DenomLoss:
+ return {
+ tag: TransactionType.DenomLoss,
+ denomLossEventId: rest[0],
+ };
default:
return undefined;
}
@@ -1603,11 +1679,6 @@ function maybeTaskFromTransaction(
tag: PendingTaskType.Purchase,
proposalId: parsedTx.proposalId,
});
- case TransactionType.Reward:
- return constructTaskIdentifier({
- tag: PendingTaskType.RewardPickup,
- walletRewardId: parsedTx.walletRewardId,
- });
case TransactionType.Refresh:
return constructTaskIdentifier({
tag: PendingTaskType.Refresh,
@@ -1636,6 +1707,9 @@ function maybeTaskFromTransaction(
tag: PendingTaskType.Recoup,
recoupGroupId: parsedTx.recoupGroupId,
});
+ case TransactionType.DenomLoss:
+ // Nothing to do for denom loss
+ return undefined;
default:
assertUnreachable(parsedTx);
}
@@ -1684,11 +1758,11 @@ async function getContextForTransaction(
return new PeerPushCreditTransactionContext(wex, tx.peerPushCreditId);
case TransactionType.Refund:
return new RefundTransactionContext(wex, tx.refundGroupId);
- case TransactionType.Reward:
- return new RewardTransactionContext(wex, tx.walletRewardId);
case TransactionType.Recoup:
+ //return new RecoupTransactionContext(ws, tx.recoupGroupId);
throw new Error("not yet supported");
- //return new RecoupTransactionContext(ws, tx.recoupGroupId);
+ case TransactionType.DenomLoss:
+ return new DenomLossTransactionContext(wex, tx.denomLossEventId);
default:
assertUnreachable(tx);
}
@@ -1847,6 +1921,27 @@ async function iterRecordsForDeposit(
}
}
+async function iterRecordsForDenomLoss(
+ tx: WalletDbReadOnlyTransaction<["denomLossEvents"]>,
+ filter: TransactionRecordFilter,
+ f: (r: DenomLossEventRecord) => Promise<void>,
+): Promise<void> {
+ let dgs: DenomLossEventRecord[];
+ if (filter.onlyState === "nonfinal") {
+ const keyRange = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+ dgs = await tx.denomLossEvents.indexes.byStatus.getAll(keyRange);
+ } else {
+ dgs = await tx.denomLossEvents.indexes.byStatus.getAll();
+ }
+
+ for (const dg of dgs) {
+ await f(dg);
+ }
+}
+
async function iterRecordsForRefund(
tx: WalletDbReadOnlyTransaction<["refundGroups"]>,
filter: TransactionRecordFilter,
diff --git a/packages/taler-wallet-core/src/versions.ts b/packages/taler-wallet-core/src/versions.ts
index bf8a9f7c8..d33a23cdd 100644
--- a/packages/taler-wallet-core/src/versions.ts
+++ b/packages/taler-wallet-core/src/versions.ts
@@ -50,11 +50,9 @@ export const WALLET_COREBANK_API_PROTOCOL_VERSION = "2:0:0";
export const WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION = "2:0:0";
/**
- * Semver of the wallet-core API implementation.
- * Will be replaced with the value from package.json in a
- * post-compilation step (inside lib/).
+ * Libtool version of the wallet-core API.
*/
-export const WALLET_CORE_API_IMPLEMENTATION_VERSION = "3:0:2";
+export const WALLET_CORE_API_PROTOCOL_VERSION = "5:0:0";
/**
* Libtool rules:
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 4f4b24b62..9a8ea8470 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -38,6 +38,8 @@ import {
ApplyDevExperimentRequest,
BackupRecovery,
BalancesResponse,
+ CanonicalizeBaseUrlRequest,
+ CanonicalizeBaseUrlResponse,
CheckPeerPullCreditRequest,
CheckPeerPullCreditResponse,
CheckPeerPushDebitRequest,
@@ -47,6 +49,7 @@ import {
ConfirmPayResult,
ConfirmPeerPullDebitRequest,
ConfirmPeerPushCreditRequest,
+ ConfirmWithdrawalRequest,
ConvertAmountRequest,
CreateDepositGroupRequest,
CreateDepositGroupResponse,
@@ -60,7 +63,7 @@ import {
FailTransactionRequest,
ForceRefreshRequest,
ForgetKnownBankAccountsRequest,
- GetActiveTasks,
+ GetActiveTasksResponse,
GetAmountRequest,
GetBalanceDetailRequest,
GetContractTermsDetailsRequest,
@@ -91,6 +94,8 @@ import {
ListGlobalCurrencyAuditorsResponse,
ListGlobalCurrencyExchangesResponse,
ListKnownBankAccountsRequest,
+ PrepareBankIntegratedWithdrawalRequest,
+ PrepareBankIntegratedWithdrawalResponse,
PrepareDepositRequest,
PrepareDepositResponse,
PreparePayRequest,
@@ -154,6 +159,7 @@ import { PaymentBalanceDetails } from "./balance.js";
export enum WalletApiOperation {
InitWallet = "initWallet",
+ SetWalletRunConfig = "setWalletRunConfig",
WithdrawTestkudos = "withdrawTestkudos",
WithdrawTestBalance = "withdrawTestBalance",
PreparePayForUri = "preparePayForUri",
@@ -194,6 +200,8 @@ export enum WalletApiOperation {
SetExchangeTosForgotten = "SetExchangeTosForgotten",
StartRefundQueryForUri = "startRefundQueryForUri",
StartRefundQuery = "startRefundQuery",
+ PrepareBankIntegratedWithdrawal = "prepareBankIntegratedWithdrawal",
+ ConfirmWithdrawal = "confirmWithdrawal",
AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal",
GetExchangeTos = "getExchangeTos",
GetExchangeDetailedInfo = "getExchangeDetailedInfo",
@@ -234,10 +242,6 @@ export enum WalletApiOperation {
Recycle = "recycle",
ApplyDevExperiment = "applyDevExperiment",
ValidateIban = "validateIban",
- TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
- TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
- TestingWaitTransactionState = "testingWaitTransactionState",
- TestingSetTimetravel = "testingSetTimetravel",
GetCurrencySpecification = "getCurrencySpecification",
ListStoredBackups = "listStoredBackups",
CreateStoredBackup = "createStoredBackup",
@@ -246,7 +250,6 @@ export enum WalletApiOperation {
UpdateExchangeEntry = "updateExchangeEntry",
ListExchangesForScopedCurrency = "listExchangesForScopedCurrency",
PrepareWithdrawExchange = "prepareWithdrawExchange",
- TestingInfiniteTransactionLoop = "testingInfiniteTransactionLoop",
GetExchangeResources = "getExchangeResources",
DeleteExchange = "deleteExchange",
ListGlobalCurrencyExchanges = "listGlobalCurrencyExchanges",
@@ -256,17 +259,28 @@ export enum WalletApiOperation {
AddGlobalCurrencyAuditor = "addGlobalCurrencyAuditor",
RemoveGlobalCurrencyAuditor = "removeGlobalCurrencyAuditor",
ListAssociatedRefreshes = "listAssociatedRefreshes",
+ Shutdown = "shutdown",
+ CanonicalizeBaseUrl = "canonicalizeBaseUrl",
+ TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
+ TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
+ TestingWaitTransactionState = "testingWaitTransactionState",
+ TestingWaitTasksDone = "testingWaitTasksDone",
+ TestingSetTimetravel = "testingSetTimetravel",
+ TestingInfiniteTransactionLoop = "testingInfiniteTransactionLoop",
TestingListTaskForTransaction = "testingListTasksForTransaction",
TestingGetDenomStats = "testingGetDenomStats",
+ TestingPing = "testingPing",
+ TestingGetReserveHistory = "testingGetReserveHistory",
}
// group: Initialization
type EmptyObject = Record<string, never>;
+
/**
* Initialize wallet-core.
*
- * Must be the request before any other operations.
+ * Must be the first request made to wallet-core.
*/
export type InitWalletOp = {
op: WalletApiOperation.InitWallet;
@@ -274,6 +288,23 @@ export type InitWalletOp = {
response: InitResponse;
};
+export type ShutdownOp = {
+ op: WalletApiOperation.Shutdown;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+/**
+ * Change the configuration of wallet-core.
+ *
+ * Currently an alias for the initWallet request.
+ */
+export type SetWalletRunConfigOp = {
+ op: WalletApiOperation.SetWalletRunConfig;
+ request: InitRequest;
+ response: InitResponse;
+};
+
export type GetVersionOp = {
op: WalletApiOperation.GetVersion;
request: EmptyObject;
@@ -454,7 +485,27 @@ export type GetWithdrawalDetailsForUriOp = {
};
/**
+ * Prepare a bank-integrated withdrawal operation.
+ */
+export type PrepareBankIntegratedWithdrawalOp = {
+ op: WalletApiOperation.PrepareBankIntegratedWithdrawal;
+ request: PrepareBankIntegratedWithdrawalRequest;
+ response: PrepareBankIntegratedWithdrawalResponse;
+};
+
+/**
+ * Confirm a withdrawal transaction.
+ */
+export type ConfirmWithdrawalOp = {
+ op: WalletApiOperation.ConfirmWithdrawal;
+ request: ConfirmWithdrawalRequest;
+ response: EmptyObject;
+};
+
+/**
* Accept a bank-integrated withdrawal.
+ *
+ * @deprecated in favor of prepare/confirm withdrawal.
*/
export type AcceptBankIntegratedWithdrawalOp = {
op: WalletApiOperation.AcceptBankIntegratedWithdrawal;
@@ -911,6 +962,12 @@ export type ValidateIbanOp = {
response: ValidateIbanResponse;
};
+export type CanonicalizeBaseUrlOp = {
+ op: WalletApiOperation.CanonicalizeBaseUrl;
+ request: CanonicalizeBaseUrlRequest;
+ response: CanonicalizeBaseUrlResponse;
+};
+
// group: Database Management
/**
@@ -1059,7 +1116,7 @@ export type GetPendingTasksOp = {
export type GetActiveTasksOp = {
op: WalletApiOperation.GetActiveTasks;
request: EmptyObject;
- response: GetActiveTasks;
+ response: GetActiveTasksResponse;
};
/**
@@ -1099,6 +1156,15 @@ export type TestingWaitTransactionsFinalOp = {
};
/**
+ * Wait until all transactions are in a final state.
+ */
+export type TestingWaitTasksDoneOp = {
+ op: WalletApiOperation.TestingWaitTasksDone;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+/**
* Wait until all refresh transactions are in a final state.
*/
export type TestingWaitRefreshesFinalOp = {
@@ -1116,6 +1182,18 @@ export type TestingWaitTransactionStateOp = {
response: EmptyObject;
};
+export type TestingPingOp = {
+ op: WalletApiOperation.TestingPing;
+ request: EmptyObject;
+ response: EmptyObject;
+};
+
+export type TestingGetReserveHistoryOp = {
+ op: WalletApiOperation.TestingGetReserveHistory;
+ request: EmptyObject;
+ response: any;
+};
+
/**
* Get stats about an exchange denomination.
*/
@@ -1147,6 +1225,7 @@ export type ForceRefreshOp = {
export type WalletOperations = {
[WalletApiOperation.InitWallet]: InitWalletOp;
+ [WalletApiOperation.SetWalletRunConfig]: SetWalletRunConfigOp;
[WalletApiOperation.GetVersion]: GetVersionOp;
[WalletApiOperation.PreparePayForUri]: PreparePayForUriOp;
[WalletApiOperation.SharePayment]: SharePaymentOp;
@@ -1232,6 +1311,7 @@ export type WalletOperations = {
[WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinalOp;
[WalletApiOperation.TestingSetTimetravel]: TestingSetTimetravelOp;
[WalletApiOperation.TestingWaitTransactionState]: TestingWaitTransactionStateOp;
+ [WalletApiOperation.TestingWaitTasksDone]: TestingWaitTasksDoneOp;
[WalletApiOperation.GetCurrencySpecification]: GetCurrencySpecificationOp;
[WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp;
[WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp;
@@ -1251,6 +1331,12 @@ export type WalletOperations = {
[WalletApiOperation.ListAssociatedRefreshes]: ListAssociatedRefreshesOp;
[WalletApiOperation.TestingListTaskForTransaction]: TestingListTasksForTransactionOp;
[WalletApiOperation.TestingGetDenomStats]: TestingGetDenomStatsOp;
+ [WalletApiOperation.TestingPing]: TestingPingOp;
+ [WalletApiOperation.Shutdown]: ShutdownOp;
+ [WalletApiOperation.PrepareBankIntegratedWithdrawal]: PrepareBankIntegratedWithdrawalOp;
+ [WalletApiOperation.ConfirmWithdrawal]: ConfirmWithdrawalOp;
+ [WalletApiOperation.CanonicalizeBaseUrl]: CanonicalizeBaseUrlOp;
+ [WalletApiOperation.TestingGetReserveHistory]: TestingGetReserveHistoryOp;
};
export type WalletCoreRequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index c203f6648..fe03dbf62 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -22,9 +22,11 @@
/**
* Imports.
*/
-import { IDBFactory } from "@gnu-taler/idb-bridge";
+import { IDBDatabase, IDBFactory } from "@gnu-taler/idb-bridge";
import {
+ AbsoluteTime,
ActiveTask,
+ AmountJson,
AmountString,
Amounts,
AsyncCondition,
@@ -35,6 +37,7 @@ import {
CreateStoredBackupResponse,
DeleteStoredBackupRequest,
DenominationInfo,
+ Duration,
ExchangesShortListResponse,
GetCurrencySpecificationResponse,
InitResponse,
@@ -52,7 +55,6 @@ import {
PrepareWithdrawExchangeRequest,
PrepareWithdrawExchangeResponse,
RecoverStoredBackupRequest,
- RetryLoopOpts,
StoredBackupList,
TalerError,
TalerErrorCode,
@@ -68,7 +70,7 @@ import {
WalletCoreVersion,
WalletNotification,
WalletRunConfig,
- WithdrawalDetailsForAmount,
+ canonicalizeBaseUrl,
checkDbInvariant,
codecForAbortTransaction,
codecForAcceptBankIntegratedWithdrawalRequest,
@@ -81,10 +83,12 @@ import {
codecForAddKnownBankAccounts,
codecForAny,
codecForApplyDevExperiment,
+ codecForCanonicalizeBaseUrlRequest,
codecForCheckPeerPullPaymentRequest,
codecForCheckPeerPushDebitRequest,
codecForConfirmPayRequest,
codecForConfirmPeerPushPaymentRequest,
+ codecForConfirmWithdrawalRequestRequest,
codecForConvertAmountRequest,
codecForCreateDepositGroupRequest,
codecForDeleteExchangeRequest,
@@ -110,6 +114,7 @@ import {
codecForIntegrationTestV2Args,
codecForListExchangesForScopedCurrencyRequest,
codecForListKnownBankAccounts,
+ codecForPrepareBankIntegratedWithdrawalRequest,
codecForPrepareDepositRequest,
codecForPreparePayRequest,
codecForPreparePayTemplateRequest,
@@ -129,6 +134,7 @@ import {
codecForSuspendTransaction,
codecForTestPayArgs,
codecForTestingGetDenomStatsRequest,
+ codecForTestingGetReserveHistoryRequest,
codecForTestingListTasksForTransactionRequest,
codecForTestingSetTimetravelRequest,
codecForTransactionByIdRequest,
@@ -143,11 +149,16 @@ import {
openPromise,
parsePaytoUri,
parseTalerUri,
+ performanceNow,
+ safeStringifyException,
sampleWalletCoreTransactions,
setDangerousTimetravel,
validateIban,
} from "@gnu-taler/taler-util";
-import type { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
+import {
+ readSuccessResponseJsonOrThrow,
+ type HttpRequestLibrary,
+} from "@gnu-taler/taler-util/http";
import {
getUserAttentions,
getUserAttentionsUnreadCount,
@@ -186,12 +197,13 @@ import {
timestampProtocolToDb,
} from "./db.js";
import {
+ checkDepositGroup,
createDepositGroup,
generateDepositGroupTxId,
- prepareDepositGroup,
} from "./deposits.js";
import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";
import {
+ ReadyExchangeSummary,
acceptExchangeTermsOfService,
addPresetExchangeEntry,
deleteExchange,
@@ -240,7 +252,12 @@ import {
checkPeerPushDebit,
initiatePeerPushDebit,
} from "./pay-peer-push-debit.js";
-import { DbAccess } from "./query.js";
+import {
+ AfterCommitInfo,
+ DbAccess,
+ DbAccessImpl,
+ TriggerSpec,
+} from "./query.js";
import { forceRefresh } from "./refresh.js";
import {
TaskScheduler,
@@ -252,6 +269,7 @@ import {
runIntegrationTest,
runIntegrationTest2,
testPay,
+ waitTasksDone,
waitTransactionState,
waitUntilAllTransactionsFinal,
waitUntilRefreshesDone,
@@ -274,7 +292,7 @@ import {
WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_COREBANK_API_PROTOCOL_VERSION,
- WALLET_CORE_API_IMPLEMENTATION_VERSION,
+ WALLET_CORE_API_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION,
WALLET_MERCHANT_PROTOCOL_VERSION,
} from "./versions.js";
@@ -285,9 +303,11 @@ import {
} from "./wallet-api-types.js";
import {
acceptWithdrawalFromUri,
+ confirmWithdrawal,
createManualWithdrawal,
- getExchangeWithdrawalInfo,
+ getWithdrawalDetailsForAmount,
getWithdrawalDetailsForUri,
+ prepareBankIntegratedWithdrawal,
} from "./withdraw.js";
const logger = new Logger("wallet.ts");
@@ -322,28 +342,31 @@ type CancelFn = () => void;
*/
async function fillDefaults(wex: WalletExecutionContext): Promise<void> {
const notifications: WalletNotification[] = [];
- await wex.db.runReadWriteTx(["config", "exchanges"], async (tx) => {
- const appliedRec = await tx.config.get("currencyDefaultsApplied");
- let alreadyApplied = appliedRec ? !!appliedRec.value : false;
- if (alreadyApplied) {
- logger.trace("defaults already applied");
- return;
- }
- for (const exch of wex.ws.config.builtin.exchanges) {
- const resp = await addPresetExchangeEntry(
- tx,
- exch.exchangeBaseUrl,
- exch.currencyHint,
- );
- if (resp.notification) {
- notifications.push(resp.notification);
+ await wex.db.runReadWriteTx(
+ { storeNames: ["config", "exchanges"] },
+ async (tx) => {
+ const appliedRec = await tx.config.get("currencyDefaultsApplied");
+ let alreadyApplied = appliedRec ? !!appliedRec.value : false;
+ if (alreadyApplied) {
+ logger.trace("defaults already applied");
+ return;
}
- }
- await tx.config.put({
- key: ConfigRecordKey.CurrencyDefaultsApplied,
- value: true,
- });
- });
+ for (const exch of wex.ws.config.builtin.exchanges) {
+ const resp = await addPresetExchangeEntry(
+ tx,
+ exch.exchangeBaseUrl,
+ exch.currencyHint,
+ );
+ if (resp.notification) {
+ notifications.push(resp.notification);
+ }
+ }
+ await tx.config.put({
+ key: ConfigRecordKey.CurrencyDefaultsApplied,
+ value: true,
+ });
+ },
+ );
for (const notif of notifications) {
wex.ws.notify(notif);
}
@@ -355,15 +378,15 @@ export async function getDenomInfo(
exchangeBaseUrl: string,
denomPubHash: string,
): Promise<DenominationInfo | undefined> {
- const key = `${exchangeBaseUrl}:${denomPubHash}`;
- const cached = wex.ws.lookupDenomCache(key);
+ const cacheKey = `${exchangeBaseUrl}:${denomPubHash}`;
+ const cached = wex.ws.denomInfoCache.get(cacheKey);
if (cached) {
return cached;
}
const d = await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
if (d) {
const denomInfo = DenominationRecord.toDenomInfo(d);
- wex.ws.putDenomCache(key, denomInfo);
+ wex.ws.denomInfoCache.put(cacheKey, denomInfo);
return denomInfo;
}
return undefined;
@@ -378,7 +401,7 @@ async function listKnownBankAccounts(
currency?: string,
): Promise<KnownBankAccounts> {
const accounts: KnownBankAccountsInfo[] = [];
- await wex.db.runReadOnlyTx(["bankAccounts"], async (tx) => {
+ await wex.db.runReadOnlyTx({ storeNames: ["bankAccounts"] }, async (tx) => {
const knownAccounts = await tx.bankAccounts.iter().toArray();
for (const r of knownAccounts) {
if (currency && currency !== r.currency) {
@@ -406,7 +429,7 @@ async function addKnownBankAccounts(
alias: string,
currency: string,
): Promise<void> {
- await wex.db.runReadWriteTx(["bankAccounts"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["bankAccounts"] }, async (tx) => {
tx.bankAccounts.put({
uri: payto,
alias: alias,
@@ -423,7 +446,7 @@ async function forgetKnownBankAccounts(
wex: WalletExecutionContext,
payto: string,
): Promise<void> {
- await wex.db.runReadWriteTx(["bankAccounts"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["bankAccounts"] }, async (tx) => {
const account = await tx.bankAccounts.get(payto);
if (!account) {
throw Error(`account not found: ${payto}`);
@@ -438,39 +461,42 @@ async function setCoinSuspended(
coinPub: string,
suspended: boolean,
): Promise<void> {
- await wex.db.runReadWriteTx(["coins", "coinAvailability"], async (tx) => {
- const c = await tx.coins.get(coinPub);
- if (!c) {
- logger.warn(`coin ${coinPub} not found, won't suspend`);
- return;
- }
- const coinAvailability = await tx.coinAvailability.get([
- c.exchangeBaseUrl,
- c.denomPubHash,
- c.maxAge,
- ]);
- checkDbInvariant(!!coinAvailability);
- if (suspended) {
- if (c.status !== CoinStatus.Fresh) {
+ await wex.db.runReadWriteTx(
+ { storeNames: ["coins", "coinAvailability"] },
+ async (tx) => {
+ const c = await tx.coins.get(coinPub);
+ if (!c) {
+ logger.warn(`coin ${coinPub} not found, won't suspend`);
return;
}
- if (coinAvailability.freshCoinCount === 0) {
- throw Error(
- `invalid coin count ${coinAvailability.freshCoinCount} in DB`,
- );
- }
- coinAvailability.freshCoinCount--;
- c.status = CoinStatus.FreshSuspended;
- } else {
- if (c.status == CoinStatus.Dormant) {
- return;
+ const coinAvailability = await tx.coinAvailability.get([
+ c.exchangeBaseUrl,
+ c.denomPubHash,
+ c.maxAge,
+ ]);
+ checkDbInvariant(!!coinAvailability);
+ if (suspended) {
+ if (c.status !== CoinStatus.Fresh) {
+ return;
+ }
+ if (coinAvailability.freshCoinCount === 0) {
+ throw Error(
+ `invalid coin count ${coinAvailability.freshCoinCount} in DB`,
+ );
+ }
+ coinAvailability.freshCoinCount--;
+ c.status = CoinStatus.FreshSuspended;
+ } else {
+ if (c.status == CoinStatus.Dormant) {
+ return;
+ }
+ coinAvailability.freshCoinCount++;
+ c.status = CoinStatus.Fresh;
}
- coinAvailability.freshCoinCount++;
- c.status = CoinStatus.Fresh;
- }
- await tx.coins.put(c);
- await tx.coinAvailability.put(coinAvailability);
- });
+ await tx.coins.put(c);
+ await tx.coinAvailability.put(coinAvailability);
+ },
+ );
}
/**
@@ -479,55 +505,58 @@ async function setCoinSuspended(
async function dumpCoins(wex: WalletExecutionContext): Promise<CoinDumpJson> {
const coinsJson: CoinDumpJson = { coins: [] };
logger.info("dumping coins");
- await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
- const coins = await tx.coins.iter().toArray();
- for (const c of coins) {
- const denom = await tx.denominations.get([
- c.exchangeBaseUrl,
- c.denomPubHash,
- ]);
- if (!denom) {
- logger.warn("no denom found for coin");
- continue;
- }
- const cs = c.coinSource;
- let refreshParentCoinPub: string | undefined;
- if (cs.type == CoinSourceType.Refresh) {
- refreshParentCoinPub = cs.oldCoinPub;
- }
- let withdrawalReservePub: string | undefined;
- if (cs.type == CoinSourceType.Withdraw) {
- withdrawalReservePub = cs.reservePub;
- }
- const denomInfo = await getDenomInfo(
- wex,
- tx,
- c.exchangeBaseUrl,
- c.denomPubHash,
- );
- if (!denomInfo) {
- logger.warn("no denomination found for coin");
- continue;
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["coins", "denominations"] },
+ async (tx) => {
+ const coins = await tx.coins.iter().toArray();
+ for (const c of coins) {
+ const denom = await tx.denominations.get([
+ c.exchangeBaseUrl,
+ c.denomPubHash,
+ ]);
+ if (!denom) {
+ logger.warn("no denom found for coin");
+ continue;
+ }
+ const cs = c.coinSource;
+ let refreshParentCoinPub: string | undefined;
+ if (cs.type == CoinSourceType.Refresh) {
+ refreshParentCoinPub = cs.oldCoinPub;
+ }
+ let withdrawalReservePub: string | undefined;
+ if (cs.type == CoinSourceType.Withdraw) {
+ withdrawalReservePub = cs.reservePub;
+ }
+ const denomInfo = await getDenomInfo(
+ wex,
+ tx,
+ c.exchangeBaseUrl,
+ c.denomPubHash,
+ );
+ if (!denomInfo) {
+ logger.warn("no denomination found for coin");
+ continue;
+ }
+ coinsJson.coins.push({
+ coin_pub: c.coinPub,
+ denom_pub: denomInfo.denomPub,
+ denom_pub_hash: c.denomPubHash,
+ denom_value: denom.value,
+ exchange_base_url: c.exchangeBaseUrl,
+ refresh_parent_coin_pub: refreshParentCoinPub,
+ withdrawal_reserve_pub: withdrawalReservePub,
+ coin_status: c.status,
+ ageCommitmentProof: c.ageCommitmentProof,
+ spend_allocation: c.spendAllocation
+ ? {
+ amount: c.spendAllocation.amount,
+ id: c.spendAllocation.id,
+ }
+ : undefined,
+ });
}
- coinsJson.coins.push({
- coin_pub: c.coinPub,
- denom_pub: denomInfo.denomPub,
- denom_pub_hash: c.denomPubHash,
- denom_value: denom.value,
- exchange_base_url: c.exchangeBaseUrl,
- refresh_parent_coin_pub: refreshParentCoinPub,
- withdrawal_reserve_pub: withdrawalReservePub,
- coin_status: c.status,
- ageCommitmentProof: c.ageCommitmentProof,
- spend_allocation: c.spendAllocation
- ? {
- amount: c.spendAllocation.amount,
- id: c.spendAllocation.id,
- }
- : undefined,
- });
- }
- });
+ },
+ );
return coinsJson;
}
@@ -661,8 +690,9 @@ export interface PendingOperationsResponse {
/**
* Implementation of the "wallet-core" API.
*/
-async function dispatchRequestInternal<Op extends WalletApiOperation>(
+async function dispatchRequestInternal(
wex: WalletExecutionContext,
+ cts: CancellationToken.Source,
operation: WalletApiOperation,
payload: unknown,
): Promise<WalletCoreResponseType<typeof operation>> {
@@ -688,23 +718,22 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await recoverStoredBackup(wex, req);
return {};
}
+ case WalletApiOperation.SetWalletRunConfig:
case WalletApiOperation.InitWallet: {
const req = codecForInitRequest().decode(payload);
logger.info(`init request: ${j2s(req)}`);
if (wex.ws.initCalled) {
- logger.warn(
- "initWallet called twice, new run configuration is ignored",
- );
- return;
+ logger.info("initializing wallet (repeat initialization)");
+ } else {
+ logger.info("initializing wallet (first initialization)");
}
- logger.trace("initializing wallet");
// Write to the DB to make sure that we're failing early in
// case the DB is not writeable.
try {
- await wex.db.runReadWriteTx(["config"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
tx.config.put({
key: ConfigRecordKey.LastInitInfo,
value: timestampProtocolToDb(TalerProtocolTimestamp.now()),
@@ -719,7 +748,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
wex.ws.initWithConfig(applyRunConfigDefaults(req.config));
- wex.ws.initCalled = true;
if (wex.ws.config.testing.skipDefaults) {
logger.trace("skipping defaults");
} else {
@@ -729,6 +757,11 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const resp: InitResponse = {
versionInfo: getVersion(wex),
};
+
+ // After initialization, task loop should run.
+ await wex.taskScheduler.ensureRunning();
+
+ wex.ws.initCalled = true;
return resp;
}
case WalletApiOperation.WithdrawTestkudos: {
@@ -794,6 +827,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
});
return {};
}
+ case WalletApiOperation.TestingPing: {
+ return {};
+ }
case WalletApiOperation.UpdateExchangeEntry: {
const req = codecForUpdateExchangeEntryRequest().decode(payload);
await fetchFreshExchange(wex, req.exchangeBaseUrl, {
@@ -808,20 +844,24 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
numLost: 0,
numOffered: 0,
};
- await wex.db.runReadOnlyTx(["denominations"], async (tx) => {
- const denoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
- req.exchangeBaseUrl,
- );
- for (const d of denoms) {
- denomStats.numKnown++;
- if (d.isOffered) {
- denomStats.numOffered++;
- }
- if (d.isLost) {
- denomStats.numLost++;
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["denominations"] },
+ async (tx) => {
+ const denoms =
+ await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+ req.exchangeBaseUrl,
+ );
+ for (const d of denoms) {
+ denomStats.numKnown++;
+ if (d.isOffered) {
+ denomStats.numOffered++;
+ }
+ if (d.isLost) {
+ denomStats.numLost++;
+ }
}
- }
- });
+ },
+ );
return denomStats;
}
case WalletApiOperation.ListExchanges: {
@@ -870,43 +910,52 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
case WalletApiOperation.GetWithdrawalDetailsForUri: {
const req = codecForGetWithdrawalDetailsForUri().decode(payload);
return await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri, {
- notifyChangeFromPendingTimeoutMs: req.notifyChangeFromPendingTimeoutMs,
restrictAge: req.restrictAge,
});
}
+ case WalletApiOperation.TestingGetReserveHistory: {
+ const req = codecForTestingGetReserveHistoryRequest().decode(payload);
+ const reserve = await wex.db.runReadOnlyTx(
+ { storeNames: ["reserves"] },
+ async (tx) => {
+ return tx.reserves.indexes.byReservePub.get(req.reservePub);
+ },
+ );
+ if (!reserve) {
+ throw Error("no reserve pub found");
+ }
+ const sigResp = await wex.cryptoApi.signReserveHistoryReq({
+ reservePriv: reserve.reservePriv,
+ startOffset: 0,
+ });
+ const exchangeBaseUrl = req.exchangeBaseUrl;
+ const url = new URL(
+ `reserves/${req.reservePub}/history`,
+ exchangeBaseUrl,
+ );
+ const resp = await wex.http.fetch(url.href, {
+ headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig },
+ });
+ const historyJson = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForAny(),
+ );
+ return historyJson;
+ }
case WalletApiOperation.AcceptManualWithdrawal: {
const req = codecForAcceptManualWithdrawalRequest().decode(payload);
const res = await createManualWithdrawal(wex, {
amount: Amounts.parseOrThrow(req.amount),
exchangeBaseUrl: req.exchangeBaseUrl,
restrictAge: req.restrictAge,
+ forceReservePriv: req.forceReservePriv,
});
return res;
}
case WalletApiOperation.GetWithdrawalDetailsForAmount: {
const req =
codecForGetWithdrawalDetailsForAmountRequest().decode(payload);
- const wi = await getExchangeWithdrawalInfo(
- wex,
- req.exchangeBaseUrl,
- Amounts.parseOrThrow(req.amount),
- req.restrictAge,
- );
- let numCoins = 0;
- for (const x of wi.selectedDenoms.selectedDenoms) {
- numCoins += x.count;
- }
- const resp: WithdrawalDetailsForAmount = {
- amountRaw: req.amount,
- amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
- paytoUris: wi.exchangePaytoUris,
- tosAccepted: wi.termsOfServiceAccepted,
- ageRestrictionOptions: wi.ageRestrictionOptions,
- withdrawalAccountsList: wi.exchangeCreditAccountDetails,
- numCoins,
- // FIXME: Once we have proper scope info support, return correct info here.
- scopeInfo: wi.scopeInfo,
- };
+ const resp = await getWithdrawalDetailsForAmount(wex, cts, req);
return resp;
}
case WalletApiOperation.GetBalances: {
@@ -954,6 +1003,20 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
restrictAge: req.restrictAge,
});
}
+ case WalletApiOperation.ConfirmWithdrawal: {
+ const req = codecForConfirmWithdrawalRequestRequest().decode(payload);
+ return confirmWithdrawal(wex, req.transactionId);
+ }
+ case WalletApiOperation.PrepareBankIntegratedWithdrawal: {
+ const req =
+ codecForPrepareBankIntegratedWithdrawalRequest().decode(payload);
+ return prepareBankIntegratedWithdrawal(wex, {
+ selectedExchange: req.exchangeBaseUrl,
+ talerWithdrawUri: req.talerWithdrawUri,
+ forcedDenomSel: req.forcedDenomSel,
+ restrictAge: req.restrictAge,
+ });
+ }
case WalletApiOperation.GetExchangeTos: {
const req = codecForGetExchangeTosRequest().decode(payload);
return getExchangeTos(
@@ -965,7 +1028,18 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
}
case WalletApiOperation.GetContractTermsDetails: {
const req = codecForGetContractTermsDetails().decode(payload);
- return getContractTermsDetails(wex, req.proposalId);
+ if (req.proposalId) {
+ // FIXME: deprecated path
+ return getContractTermsDetails(wex, req.proposalId);
+ }
+ if (req.transactionId) {
+ const parsedTx = parseTransactionIdentifier(req.transactionId);
+ if (parsedTx?.tag === TransactionType.Payment) {
+ return getContractTermsDetails(wex, parsedTx.proposalId);
+ }
+ throw Error("transactionId is not a payment transaction");
+ }
+ throw Error("transactionId missing");
}
case WalletApiOperation.RetryPendingNow: {
logger.error("retryPendingNow currently not implemented");
@@ -1018,8 +1092,8 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const tasksInfo = await Promise.all(
allTasksId.map(async (id) => {
- return await wex.ws.db.runReadOnlyTx(
- ["operationRetries"],
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["operationRetries"] },
async (tx) => {
return tx.operationRetries.get(id);
},
@@ -1041,8 +1115,8 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const lastError = d?.lastError;
return {
- id: taskId,
- counter,
+ taskId: taskId,
+ retryCounter: counter,
firstTry,
nextTry,
lastError,
@@ -1193,7 +1267,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
}
case WalletApiOperation.PrepareDeposit: {
const req = codecForPrepareDepositRequest().decode(payload);
- return await prepareDepositGroup(wex, req);
+ return await checkDepositGroup(wex, req);
}
case WalletApiOperation.GenerateDepositGroupTxId:
return {
@@ -1221,9 +1295,11 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
case WalletApiOperation.TestCrypto: {
return await wex.cryptoApi.hashString({ str: "hello world" });
}
- case WalletApiOperation.ClearDb:
+ case WalletApiOperation.ClearDb: {
+ wex.ws.clearAllCaches();
await clearDatabase(wex.db.idbHandle());
return {};
+ }
case WalletApiOperation.Recycle: {
throw Error("not implemented");
return {};
@@ -1236,102 +1312,136 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const resp: ListGlobalCurrencyExchangesResponse = {
exchanges: [],
};
- await wex.db.runReadOnlyTx(["globalCurrencyExchanges"], async (tx) => {
- const gceList = await tx.globalCurrencyExchanges.iter().toArray();
- for (const gce of gceList) {
- resp.exchanges.push({
- currency: gce.currency,
- exchangeBaseUrl: gce.exchangeBaseUrl,
- exchangeMasterPub: gce.exchangeMasterPub,
- });
- }
- });
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["globalCurrencyExchanges"] },
+ async (tx) => {
+ const gceList = await tx.globalCurrencyExchanges.iter().toArray();
+ for (const gce of gceList) {
+ resp.exchanges.push({
+ currency: gce.currency,
+ exchangeBaseUrl: gce.exchangeBaseUrl,
+ exchangeMasterPub: gce.exchangeMasterPub,
+ });
+ }
+ },
+ );
return resp;
}
case WalletApiOperation.ListGlobalCurrencyAuditors: {
const resp: ListGlobalCurrencyAuditorsResponse = {
auditors: [],
};
- await wex.db.runReadOnlyTx(["globalCurrencyAuditors"], async (tx) => {
- const gcaList = await tx.globalCurrencyAuditors.iter().toArray();
- for (const gca of gcaList) {
- resp.auditors.push({
- currency: gca.currency,
- auditorBaseUrl: gca.auditorBaseUrl,
- auditorPub: gca.auditorPub,
- });
- }
- });
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["globalCurrencyAuditors"] },
+ async (tx) => {
+ const gcaList = await tx.globalCurrencyAuditors.iter().toArray();
+ for (const gca of gcaList) {
+ resp.auditors.push({
+ currency: gca.currency,
+ auditorBaseUrl: gca.auditorBaseUrl,
+ auditorPub: gca.auditorPub,
+ });
+ }
+ },
+ );
return resp;
}
case WalletApiOperation.AddGlobalCurrencyExchange: {
const req = codecForAddGlobalCurrencyExchangeRequest().decode(payload);
- await wex.db.runReadWriteTx(["globalCurrencyExchanges"], async (tx) => {
- const key = [req.currency, req.exchangeBaseUrl, req.exchangeMasterPub];
- const existingRec =
- await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get(
- key,
- );
- if (existingRec) {
- return;
- }
- await tx.globalCurrencyExchanges.add({
- currency: req.currency,
- exchangeBaseUrl: req.exchangeBaseUrl,
- exchangeMasterPub: req.exchangeMasterPub,
- });
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["globalCurrencyExchanges"] },
+ async (tx) => {
+ const key = [
+ req.currency,
+ req.exchangeBaseUrl,
+ req.exchangeMasterPub,
+ ];
+ const existingRec =
+ await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get(
+ key,
+ );
+ if (existingRec) {
+ return;
+ }
+ wex.ws.exchangeCache.clear();
+ await tx.globalCurrencyExchanges.add({
+ currency: req.currency,
+ exchangeBaseUrl: req.exchangeBaseUrl,
+ exchangeMasterPub: req.exchangeMasterPub,
+ });
+ },
+ );
return {};
}
case WalletApiOperation.RemoveGlobalCurrencyExchange: {
const req = codecForRemoveGlobalCurrencyExchangeRequest().decode(payload);
- await wex.db.runReadWriteTx(["globalCurrencyExchanges"], async (tx) => {
- const key = [req.currency, req.exchangeBaseUrl, req.exchangeMasterPub];
- const existingRec =
- await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get(
- key,
- );
- if (!existingRec) {
- return;
- }
- checkDbInvariant(!!existingRec.id);
- await tx.globalCurrencyExchanges.delete(existingRec.id);
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["globalCurrencyExchanges"] },
+ async (tx) => {
+ const key = [
+ req.currency,
+ req.exchangeBaseUrl,
+ req.exchangeMasterPub,
+ ];
+ const existingRec =
+ await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get(
+ key,
+ );
+ if (!existingRec) {
+ return;
+ }
+ wex.ws.exchangeCache.clear();
+ checkDbInvariant(!!existingRec.id);
+ await tx.globalCurrencyExchanges.delete(existingRec.id);
+ },
+ );
return {};
}
case WalletApiOperation.AddGlobalCurrencyAuditor: {
const req = codecForAddGlobalCurrencyAuditorRequest().decode(payload);
- await wex.db.runReadWriteTx(["globalCurrencyAuditors"], async (tx) => {
- const key = [req.currency, req.auditorBaseUrl, req.auditorPub];
- const existingRec =
- await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get(
- key,
- );
- if (existingRec) {
- return;
- }
- await tx.globalCurrencyAuditors.add({
- currency: req.currency,
- auditorBaseUrl: req.auditorBaseUrl,
- auditorPub: req.auditorPub,
- });
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["globalCurrencyAuditors"] },
+ async (tx) => {
+ const key = [req.currency, req.auditorBaseUrl, req.auditorPub];
+ const existingRec =
+ await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get(
+ key,
+ );
+ if (existingRec) {
+ return;
+ }
+ await tx.globalCurrencyAuditors.add({
+ currency: req.currency,
+ auditorBaseUrl: req.auditorBaseUrl,
+ auditorPub: req.auditorPub,
+ });
+ wex.ws.exchangeCache.clear();
+ },
+ );
+ return {};
+ }
+ case WalletApiOperation.TestingWaitTasksDone: {
+ await waitTasksDone(wex);
return {};
}
case WalletApiOperation.RemoveGlobalCurrencyAuditor: {
const req = codecForRemoveGlobalCurrencyAuditorRequest().decode(payload);
- await wex.db.runReadWriteTx(["globalCurrencyAuditors"], async (tx) => {
- const key = [req.currency, req.auditorBaseUrl, req.auditorPub];
- const existingRec =
- await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get(
- key,
- );
- if (!existingRec) {
- return;
- }
- checkDbInvariant(!!existingRec.id);
- await tx.globalCurrencyAuditors.delete(existingRec.id);
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["globalCurrencyAuditors"] },
+ async (tx) => {
+ const key = [req.currency, req.auditorBaseUrl, req.auditorPub];
+ const existingRec =
+ await tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get(
+ key,
+ );
+ if (!existingRec) {
+ return;
+ }
+ checkDbInvariant(!!existingRec.id);
+ await tx.globalCurrencyAuditors.delete(existingRec.id);
+ wex.ws.exchangeCache.clear();
+ },
+ );
return {};
}
case WalletApiOperation.ImportDb: {
@@ -1376,6 +1486,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await applyDevExperiment(wex, req.devExperimentUri);
return {};
}
+ case WalletApiOperation.Shutdown: {
+ wex.ws.stop();
+ return {};
+ }
case WalletApiOperation.GetVersion: {
return getVersion(wex);
}
@@ -1386,7 +1500,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
case WalletApiOperation.TestingSetTimetravel: {
const req = codecForTestingSetTimetravelRequest().decode(payload);
setDangerousTimetravel(req.offsetMs);
- wex.taskScheduler.reload();
+ await wex.taskScheduler.reload();
return {};
}
case WalletApiOperation.DeleteExchange: {
@@ -1398,6 +1512,12 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const req = codecForGetExchangeResourcesRequest().decode(payload);
return await getExchangeResources(wex, req.exchangeBaseUrl);
}
+ case WalletApiOperation.CanonicalizeBaseUrl: {
+ const req = codecForCanonicalizeBaseUrlRequest().decode(payload);
+ return {
+ url: canonicalizeBaseUrl(req.url),
+ };
+ }
case WalletApiOperation.TestingInfiniteTransactionLoop: {
const myDelayMs = (payload as any).delayMs ?? 5;
const shouldFetch = !!(payload as any).shouldFetch;
@@ -1417,7 +1537,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
let loopCount = 0;
while (true) {
logger.info(`looping test write tx, iteration ${loopCount}`);
- await wex.db.runReadWriteTx(["config"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["config"] }, async (tx) => {
await tx.config.put({
key: ConfigRecordKey.TestLoopTx,
value: loopCount,
@@ -1448,14 +1568,14 @@ export function getVersion(wex: WalletExecutionContext): WalletCoreVersion {
implementationSemver: walletCoreBuildInfo.implementationSemver,
implementationGitHash: walletCoreBuildInfo.implementationGitHash,
hash: undefined,
- version: WALLET_CORE_API_IMPLEMENTATION_VERSION,
+ version: WALLET_CORE_API_PROTOCOL_VERSION,
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
bankIntegrationApiRange: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
corebankApiRange: WALLET_COREBANK_API_PROTOCOL_VERSION,
bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
- devMode: false,
+ devMode: wex.ws.config.testing.devModeActive,
};
return result;
}
@@ -1464,7 +1584,7 @@ export function getObservedWalletExecutionContext(
ws: InternalWalletState,
cancellationToken: CancellationToken,
oc: ObservabilityContext,
-) {
+): WalletExecutionContext {
const wex: WalletExecutionContext = {
ws,
cancellationToken,
@@ -1481,7 +1601,7 @@ export function getNormalWalletExecutionContext(
ws: InternalWalletState,
cancellationToken: CancellationToken,
oc: ObservabilityContext,
-) {
+): WalletExecutionContext {
const wex: WalletExecutionContext = {
ws,
cancellationToken,
@@ -1511,6 +1631,8 @@ async function handleCoreApiRequest(
let wex: WalletExecutionContext;
let oc: ObservabilityContext;
+ const cts = CancellationToken.create();
+
if (ws.initCalled && ws.config.testing.emitObservabilityEvents) {
oc = {
observe(evt) {
@@ -1523,26 +1645,30 @@ async function handleCoreApiRequest(
},
};
- wex = getObservedWalletExecutionContext(ws, CancellationToken.CONTINUE, oc);
+ wex = getObservedWalletExecutionContext(ws, cts.token, oc);
} else {
oc = {
observe(evt) {},
};
- wex = getNormalWalletExecutionContext(ws, CancellationToken.CONTINUE, oc);
+ wex = getNormalWalletExecutionContext(ws, cts.token, oc);
}
try {
+ const start = performanceNow();
await ws.ensureWalletDbOpen();
oc.observe({
type: ObservabilityEventType.RequestStart,
});
const result = await dispatchRequestInternal(
wex,
+ cts,
operation as any,
payload,
);
+ const end = performanceNow();
oc.observe({
type: ObservabilityEventType.RequestFinishSuccess,
+ durationMs: Number((end - start) / 1000n / 1000n),
});
return {
type: "response",
@@ -1638,15 +1764,6 @@ export class Wallet {
return this.ws.addNotificationListener(f);
}
- stop(): void {
- this.ws.stop();
- }
-
- async runTaskLoop(opts?: RetryLoopOpts): Promise<void> {
- await this.ws.ensureWalletDbOpen();
- return this.ws.taskScheduler.run(opts);
- }
-
async handleCoreApiRequest(
operation: string,
id: string,
@@ -1657,6 +1774,76 @@ export class Wallet {
}
}
+export interface DevExperimentState {
+ blockRefreshes?: boolean;
+}
+
+export class Cache<T> {
+ private map: Map<string, [AbsoluteTime, T]> = new Map();
+
+ constructor(
+ private maxCapacity: number,
+ private cacheDuration: Duration,
+ ) {}
+
+ get(key: string): T | undefined {
+ const r = this.map.get(key);
+ if (!r) {
+ return undefined;
+ }
+
+ if (AbsoluteTime.isExpired(r[0])) {
+ this.map.delete(key);
+ return undefined;
+ }
+
+ return r[1];
+ }
+
+ clear(): void {
+ this.map.clear();
+ }
+
+ put(key: string, value: T): void {
+ if (this.map.size > this.maxCapacity) {
+ this.map.clear();
+ }
+ const expiry = AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ this.cacheDuration,
+ );
+ this.map.set(key, [expiry, value]);
+ }
+}
+
+/**
+ * Implementation of triggers for the wallet DB.
+ */
+class WalletDbTriggerSpec implements TriggerSpec {
+ constructor(public ws: InternalWalletState) {}
+
+ afterCommit(info: AfterCommitInfo): void {
+ if (info.mode !== "readwrite") {
+ return;
+ }
+ logger.info(
+ `in after commit callback for readwrite, modified ${j2s([
+ ...info.modifiedStores,
+ ])}`,
+ );
+ const modified = info.accessedStores;
+ if (
+ modified.has(WalletStoresV1.exchanges.storeName) ||
+ modified.has(WalletStoresV1.exchangeDetails.storeName) ||
+ modified.has(WalletStoresV1.denominations.storeName) ||
+ modified.has(WalletStoresV1.globalCurrencyAuditors.storeName) ||
+ modified.has(WalletStoresV1.globalCurrencyExchanges.storeName)
+ ) {
+ this.ws.clearAllCaches();
+ }
+ }
+}
+
/**
* Internal state of the wallet.
*
@@ -1674,7 +1861,20 @@ export class InternalWalletState {
initCalled = false;
- private denomCache: Map<string, DenominationInfo> = new Map();
+ refreshCostCache: Cache<AmountJson> = new Cache(
+ 1000,
+ Duration.fromSpec({ minutes: 1 }),
+ );
+
+ denomInfoCache: Cache<DenominationInfo> = new Cache(
+ 1000,
+ Duration.fromSpec({ minutes: 1 }),
+ );
+
+ exchangeCache: Cache<ReadyExchangeSummary> = new Cache(
+ 1000,
+ Duration.fromSpec({ minutes: 1 }),
+ );
/**
* Promises that are waiting for a particular resource.
@@ -1690,34 +1890,36 @@ export class InternalWalletState {
private _config: Readonly<WalletRunConfig> | undefined;
- private _db: DbAccess<typeof WalletStoresV1> | undefined = undefined;
+ private _indexedDbHandle: IDBDatabase | undefined = undefined;
+
+ private _dbAccessHandle: DbAccess<typeof WalletStoresV1> | undefined;
private _http: HttpRequestLibrary | undefined = undefined;
get db(): DbAccess<typeof WalletStoresV1> {
- if (!this._db) {
- throw Error("db not initialized");
+ if (!this._dbAccessHandle) {
+ this._dbAccessHandle = this.createDbAccessHandle(
+ CancellationToken.CONTINUE,
+ );
}
- return this._db;
+ return this._dbAccessHandle;
}
- lookupDenomCache(denomCacheKey: string): DenominationInfo | undefined {
- return this.denomCache.get(denomCacheKey);
- }
+ devExperimentState: DevExperimentState = {};
- putDenomCache(denomCacheKey: string, denomInfo: DenominationInfo): void {
- if (this.denomCache.size > 1000) {
- this.denomCache.clear();
- }
- this.denomCache.set(denomCacheKey, denomInfo);
+ clientCancellationMap: Map<string, CancellationToken.Source> = new Map();
+
+ clearAllCaches(): void {
+ this.exchangeCache.clear();
+ this.denomInfoCache.clear();
+ this.refreshCostCache.clear();
}
initWithConfig(newConfig: WalletRunConfig): void {
- if (this._config) {
- throw Error("config already set");
- }
this._config = newConfig;
+ logger.info(`setting new config to ${j2s(newConfig)}`);
+
this._http = this.httpFactory(newConfig);
if (this.config.testing.devModeActive) {
@@ -1725,6 +1927,20 @@ export class InternalWalletState {
}
}
+ createDbAccessHandle(
+ cancellationToken: CancellationToken,
+ ): DbAccess<typeof WalletStoresV1> {
+ if (!this._indexedDbHandle) {
+ throw Error("db not initialized");
+ }
+ return new DbAccessImpl(
+ this._indexedDbHandle,
+ WalletStoresV1,
+ new WalletDbTriggerSpec(this),
+ cancellationToken,
+ );
+ }
+
get config(): WalletRunConfig {
if (!this._config) {
throw Error("config not initialized");
@@ -1751,7 +1967,7 @@ export class InternalWalletState {
}
async ensureWalletDbOpen(): Promise<void> {
- if (this._db) {
+ if (this._indexedDbHandle) {
return;
}
const myVersionChange = async (): Promise<void> => {
@@ -1759,7 +1975,7 @@ export class InternalWalletState {
};
try {
const myDb = await openTalerDatabase(this.idb, myVersionChange);
- this._db = myDb;
+ this._indexedDbHandle = myDb;
} catch (e) {
logger.error("error writing to database during initialization");
throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, {
@@ -1796,6 +2012,9 @@ export class InternalWalletState {
this.stopped = true;
this.timerGroup.stopCurrentAndFutureTimers();
this.cryptoDispatcher.stop();
+ this.taskScheduler.shutdown().catch((e) => {
+ logger.warn(`shutdown failed: ${safeStringifyException(e)}`);
+ });
}
/**
@@ -1830,7 +2049,7 @@ export class InternalWalletState {
} finally {
for (const token of tokens) {
this.resourceLocks.delete(token);
- let waiter = (this.resourceWaiters[token] ?? []).shift();
+ const waiter = (this.resourceWaiters[token] ?? []).shift();
if (waiter) {
waiter.resolve();
}
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index 8767c1eca..81e104014 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -41,6 +41,7 @@ import {
DenomSelItem,
DenomSelectionState,
Duration,
+ EddsaPrivateKeyString,
ExchangeBatchWithdrawRequest,
ExchangeUpdateStatus,
ExchangeWireAccount,
@@ -49,16 +50,18 @@ import {
ExchangeWithdrawResponse,
ExchangeWithdrawalDetails,
ForcedDenomSel,
+ GetWithdrawalDetailsForAmountRequest,
HttpStatusCode,
LibtoolVersion,
Logger,
NotificationType,
+ ObservabilityEventType,
+ PrepareBankIntegratedWithdrawalResponse,
TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
TalerErrorDetail,
TalerPreciseTimestamp,
- TalerProtocolTimestamp,
Transaction,
TransactionAction,
TransactionIdStr,
@@ -70,14 +73,15 @@ import {
UnblindedSignature,
WalletNotification,
WithdrawUriInfoResponse,
+ WithdrawalDetailsForAmount,
WithdrawalExchangeAccountDetails,
WithdrawalType,
addPaytoQueryParams,
assertUnreachable,
- canonicalizeBaseUrl,
checkDbInvariant,
checkLogicInvariant,
codeForBankWithdrawalOperationPostResponse,
+ codecForBankWithdrawalOperationStatus,
codecForCashinConversionResponse,
codecForConversionBankConfig,
codecForExchangeWithdrawBatchResponse,
@@ -129,6 +133,7 @@ import {
WithdrawalGroupRecord,
WithdrawalGroupStatus,
WithdrawalRecordType,
+ timestampAbsoluteFromDb,
timestampPreciseFromDb,
timestampPreciseToDb,
} from "./db.js";
@@ -151,6 +156,7 @@ import {
constructTransactionIdentifier,
isUnsuccessfulTransaction,
notifyTransition,
+ parseTransactionIdentifier,
} from "./transactions.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@@ -161,7 +167,7 @@ import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
/**
* Logger for this file.
*/
-const logger = new Logger("operations/withdraw.ts");
+const logger = new Logger("withdraw.ts");
/**
* Update the materialized withdrawal transaction based
@@ -323,7 +329,7 @@ export class WithdrawTransactionContext implements TransactionContext {
? [...baseStores, ...opts.extraStores]
: baseStores;
const transitionInfo = await this.wex.db.runReadWriteTx(
- stores,
+ { storeNames: stores },
async (tx) => {
const wgRec = await tx.withdrawalGroups.get(this.withdrawalGroupId);
let oldTxState: TransactionState;
@@ -463,13 +469,18 @@ export class WithdrawTransactionContext implements TransactionContext {
break;
case WithdrawalGroupStatus.SuspendedAbortingBank:
case WithdrawalGroupStatus.AbortingBank:
+ case WithdrawalGroupStatus.AbortedUserRefused:
// No transition needed, but not an error
return TransitionResult.stay();
+ case WithdrawalGroupStatus.DialogProposed:
+ newStatus = WithdrawalGroupStatus.AbortedUserRefused;
+ break;
case WithdrawalGroupStatus.Done:
case WithdrawalGroupStatus.FailedBankAborted:
case WithdrawalGroupStatus.AbortedExchange:
case WithdrawalGroupStatus.AbortedBank:
case WithdrawalGroupStatus.FailedAbortingBank:
+ case WithdrawalGroupStatus.AbortedOtherWallet:
// Not allowed
throw Error("abort not allowed in current state");
default:
@@ -655,6 +666,21 @@ export function computeWithdrawalTransactionStatus(
major: TransactionMajorState.Aborted,
minor: TransactionMinorState.Bank,
};
+ case WithdrawalGroupStatus.AbortedUserRefused:
+ return {
+ major: TransactionMajorState.Aborted,
+ minor: TransactionMinorState.Refused,
+ };
+ case WithdrawalGroupStatus.DialogProposed:
+ return {
+ major: TransactionMajorState.Dialog,
+ minor: TransactionMinorState.Proposed,
+ };
+ case WithdrawalGroupStatus.AbortedOtherWallet:
+ return {
+ major: TransactionMajorState.Aborted,
+ minor: TransactionMinorState.CompletedByOtherWallet,
+ };
}
}
@@ -699,12 +725,76 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.SuspendedKyc:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.FailedAbortingBank:
- return [TransactionAction.Delete];
case WithdrawalGroupStatus.AbortedExchange:
- return [TransactionAction.Delete];
case WithdrawalGroupStatus.AbortedBank:
+ case WithdrawalGroupStatus.AbortedOtherWallet:
+ case WithdrawalGroupStatus.AbortedUserRefused:
return [TransactionAction.Delete];
+ case WithdrawalGroupStatus.DialogProposed:
+ return [TransactionAction.Abort];
+ }
+}
+
+async function processWithdrawalGroupDialogProposed(
+ ctx: WithdrawTransactionContext,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<TaskRunResult> {
+ if (
+ withdrawalGroup.wgInfo.withdrawalType !==
+ WithdrawalRecordType.BankIntegrated
+ ) {
+ throw new Error(
+ "processWithdrawalGroupDialogProposed called in unexpected state",
+ );
+ }
+
+ const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri;
+
+ const parsedUri = parseWithdrawUri(talerWithdrawUri);
+
+ checkLogicInvariant(!!parsedUri);
+
+ const wopid = parsedUri.withdrawalOperationId;
+
+ const url = new URL(
+ `withdrawal-operation/${wopid}`,
+ parsedUri.bankIntegrationApiBaseUrl,
+ );
+
+ url.searchParams.set("old_state", "pending");
+ url.searchParams.set("long_poll_ms", "30000");
+
+ const resp = await ctx.wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: ctx.wex.cancellationToken,
+ });
+
+ // If the bank claims that the withdrawal operation is already
+ // pending, but we're still in DialogProposed, some other wallet
+ // must've completed the withdrawal, we're giving up.
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ const body = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForBankWithdrawalOperationStatus(),
+ );
+ if (body.status !== "pending") {
+ await ctx.transition({}, async (rec) => {
+ switch (rec?.status) {
+ case WithdrawalGroupStatus.DialogProposed: {
+ rec.status = WithdrawalGroupStatus.AbortedOtherWallet;
+ return TransitionResult.transition(rec);
+ }
+ }
+ return TransitionResult.stay();
+ });
+ }
+ break;
+ }
}
+
+ return TaskRunResult.longpollReturnedPending();
}
/**
@@ -749,8 +839,6 @@ export async function getBankWithdrawalInfo(
}
const { body: status } = resp;
- logger.info(`bank withdrawal operation status: ${j2s(status)}`);
-
return {
operationId: uriResult.withdrawalOperationId,
apiBaseUrl: uriResult.bankIntegrationApiBaseUrl,
@@ -771,9 +859,12 @@ async function getCandidateWithdrawalDenoms(
exchangeBaseUrl: string,
currency: string,
): Promise<DenominationRecord[]> {
- return await wex.db.runReadOnlyTx(["denominations"], async (tx) => {
- return getCandidateWithdrawalDenomsTx(wex, tx, exchangeBaseUrl, currency);
- });
+ return await wex.db.runReadOnlyTx(
+ { storeNames: ["denominations"] },
+ async (tx) => {
+ return getCandidateWithdrawalDenomsTx(wex, tx, exchangeBaseUrl, currency);
+ },
+ );
}
export async function getCandidateWithdrawalDenomsTx(
@@ -804,12 +895,15 @@ async function processPlanchetGenerate(
withdrawalGroup: WithdrawalGroupRecord,
coinIdx: number,
): Promise<void> {
- let planchet = await wex.db.runReadOnlyTx(["planchets"], async (tx) => {
- return tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- coinIdx,
- ]);
- });
+ let planchet = await wex.db.runReadOnlyTx(
+ { storeNames: ["planchets"] },
+ async (tx) => {
+ return tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ coinIdx,
+ ]);
+ },
+ );
if (planchet) {
return;
}
@@ -835,9 +929,17 @@ async function processPlanchetGenerate(
}
const denomPubHash = maybeDenomPubHash;
- const denom = await wex.db.runReadOnlyTx(["denominations"], async (tx) => {
- return getDenomInfo(wex, tx, withdrawalGroup.exchangeBaseUrl, denomPubHash);
- });
+ const denom = await wex.db.runReadOnlyTx(
+ { storeNames: ["denominations"] },
+ async (tx) => {
+ return getDenomInfo(
+ wex,
+ tx,
+ withdrawalGroup.exchangeBaseUrl,
+ denomPubHash,
+ );
+ },
+ );
checkDbInvariant(!!denom);
const r = await wex.cryptoApi.createPlanchet({
denomPub: denom.denomPub,
@@ -863,7 +965,7 @@ async function processPlanchetGenerate(
ageCommitmentProof: r.ageCommitmentProof,
lastError: undefined,
};
- await wex.db.runReadWriteTx(["planchets"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["planchets"] }, async (tx) => {
const p = await tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroup.withdrawalGroupId,
coinIdx,
@@ -1006,48 +1108,51 @@ async function processPlanchetExchangeBatchRequest(
// Indices of coins that are included in the batch request
const requestCoinIdxs: number[] = [];
- await wex.db.runReadOnlyTx(["planchets", "denominations"], async (tx) => {
- for (
- let coinIdx = args.coinStartIndex;
- coinIdx < args.coinStartIndex + args.batchSize &&
- coinIdx < wgContext.numPlanchets;
- coinIdx++
- ) {
- let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- coinIdx,
- ]);
- if (!planchet) {
- continue;
- }
- if (planchet.planchetStatus === PlanchetStatus.WithdrawalDone) {
- logger.warn("processPlanchet: planchet already withdrawn");
- continue;
- }
- if (planchet.planchetStatus === PlanchetStatus.AbortedReplaced) {
- continue;
- }
- const denom = await getDenomInfo(
- wex,
- tx,
- withdrawalGroup.exchangeBaseUrl,
- planchet.denomPubHash,
- );
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["planchets", "denominations"] },
+ async (tx) => {
+ for (
+ let coinIdx = args.coinStartIndex;
+ coinIdx < args.coinStartIndex + args.batchSize &&
+ coinIdx < wgContext.numPlanchets;
+ coinIdx++
+ ) {
+ let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ coinIdx,
+ ]);
+ if (!planchet) {
+ continue;
+ }
+ if (planchet.planchetStatus === PlanchetStatus.WithdrawalDone) {
+ logger.warn("processPlanchet: planchet already withdrawn");
+ continue;
+ }
+ if (planchet.planchetStatus === PlanchetStatus.AbortedReplaced) {
+ continue;
+ }
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ withdrawalGroup.exchangeBaseUrl,
+ planchet.denomPubHash,
+ );
- if (!denom) {
- logger.error("db inconsistent: denom for planchet not found");
- continue;
- }
+ if (!denom) {
+ logger.error("db inconsistent: denom for planchet not found");
+ continue;
+ }
- const planchetReq: ExchangeWithdrawRequest = {
- denom_pub_hash: planchet.denomPubHash,
- reserve_sig: planchet.withdrawSig,
- coin_ev: planchet.coinEv,
- };
- batchReq.planchets.push(planchetReq);
- requestCoinIdxs.push(coinIdx);
- }
- });
+ const planchetReq: ExchangeWithdrawRequest = {
+ denom_pub_hash: planchet.denomPubHash,
+ reserve_sig: planchet.withdrawSig,
+ coin_ev: planchet.coinEv,
+ };
+ batchReq.planchets.push(planchetReq);
+ requestCoinIdxs.push(coinIdx);
+ }
+ },
+ );
if (batchReq.planchets.length == 0) {
logger.warn("empty withdrawal batch");
@@ -1062,7 +1167,7 @@ async function processPlanchetExchangeBatchRequest(
coinIdx: number,
): Promise<void> {
logger.trace(`withdrawal request failed: ${j2s(errDetail)}`);
- await wex.db.runReadWriteTx(["planchets"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["planchets"] }, async (tx) => {
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroup.withdrawalGroupId,
coinIdx,
@@ -1134,7 +1239,7 @@ async function processPlanchetVerifyAndStoreCoin(
const withdrawalGroup = wgContext.wgRecord;
logger.trace(`checking and storing planchet idx=${coinIdx}`);
const d = await wex.db.runReadOnlyTx(
- ["planchets", "denominations"],
+ { storeNames: ["planchets", "denominations"] },
async (tx) => {
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroup.withdrawalGroupId,
@@ -1198,7 +1303,7 @@ async function processPlanchetVerifyAndStoreCoin(
});
if (!isValid) {
- await wex.db.runReadWriteTx(["planchets"], async (tx) => {
+ await wex.db.runReadWriteTx({ storeNames: ["planchets"] }, async (tx) => {
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
withdrawalGroup.withdrawalGroupId,
coinIdx,
@@ -1252,7 +1357,7 @@ async function processPlanchetVerifyAndStoreCoin(
wgContext.planchetsFinished.add(planchet.coinPub);
await wex.db.runReadWriteTx(
- ["planchets", "coins", "coinAvailability", "denominations"],
+ { storeNames: ["planchets", "coins", "coinAvailability", "denominations"] },
async (tx) => {
const p = await tx.planchets.get(planchetCoinPub);
if (!p || p.planchetStatus === PlanchetStatus.WithdrawalDone) {
@@ -1278,7 +1383,7 @@ export async function updateWithdrawalDenoms(
`updating denominations used for withdrawal for ${exchangeBaseUrl}`,
);
const exchangeDetails = await wex.db.runReadOnlyTx(
- ["exchanges", "exchangeDetails"],
+ { storeNames: ["exchanges", "exchangeDetails"] },
async (tx) => {
return getExchangeWireDetailsInTx(tx, exchangeBaseUrl);
},
@@ -1341,12 +1446,16 @@ export async function updateWithdrawalDenoms(
}
if (updatedDenominations.length > 0) {
logger.trace("writing denomination batch to db");
- await wex.db.runReadWriteTx(["denominations"], async (tx) => {
- for (let i = 0; i < updatedDenominations.length; i++) {
- const denom = updatedDenominations[i];
- await tx.denominations.put(denom);
- }
- });
+ await wex.db.runReadWriteTx(
+ { storeNames: ["denominations"] },
+ async (tx) => {
+ for (let i = 0; i < updatedDenominations.length; i++) {
+ const denom = updatedDenominations[i];
+ await tx.denominations.put(denom);
+ }
+ },
+ );
+ wex.ws.denomInfoCache.clear();
logger.trace("done with DB write");
}
}
@@ -1557,7 +1666,7 @@ async function redenominateWithdrawal(
): Promise<void> {
logger.trace(`redenominating withdrawal group ${withdrawalGroupId}`);
await wex.db.runReadWriteTx(
- ["withdrawalGroups", "planchets", "denominations"],
+ { storeNames: ["withdrawalGroups", "planchets", "denominations"] },
async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) {
@@ -1583,6 +1692,8 @@ async function redenominateWithdrawal(
let amountRemaining = zero;
let prevTotalCoinValue = zero;
let prevTotalWithdrawalCost = zero;
+ let prevHasDenomWithAgeRestriction = false;
+ let prevEarliestDepositExpiration = AbsoluteTime.never();
let prevDenoms: DenomSelItem[] = [];
let coinIndex = 0;
for (let i = 0; i < oldSel.selectedDenoms.length; i++) {
@@ -1615,6 +1726,12 @@ async function redenominateWithdrawal(
denomPubHash: sel.denomPubHash,
skip: sel.skip,
});
+ prevHasDenomWithAgeRestriction =
+ prevHasDenomWithAgeRestriction || denom.denomPub.age_mask > 0;
+ prevEarliestDepositExpiration = AbsoluteTime.min(
+ prevEarliestDepositExpiration,
+ timestampAbsoluteFromDb(denom.stampExpireDeposit),
+ );
} else {
amountRemaining = amountRemaining.add(denomValue, denomFeeWithdraw);
prevDenoms.push({
@@ -1663,6 +1780,16 @@ async function redenominateWithdrawal(
totalWithdrawCost: zero
.add(prevTotalWithdrawalCost, newSel.totalWithdrawCost)
.toString(),
+ hasDenomWithAgeRestriction:
+ prevHasDenomWithAgeRestriction || newSel.hasDenomWithAgeRestriction,
+ earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.min(
+ prevEarliestDepositExpiration,
+ AbsoluteTime.fromProtocolTimestamp(
+ newSel.earliestDepositExpiration,
+ ),
+ ),
+ ),
};
wg.denomsSel = mergedSel;
if (logger.shouldLogTrace()) {
@@ -1707,7 +1834,7 @@ async function processWithdrawalGroupPendingReady(
wgRecord: withdrawalGroup,
};
- await wex.db.runReadOnlyTx(["planchets"], async (tx) => {
+ await wex.db.runReadOnlyTx({ storeNames: ["planchets"] }, async (tx) => {
const planchets =
await tx.planchets.indexes.byGroup.getAll(withdrawalGroupId);
for (const p of planchets) {
@@ -1751,7 +1878,7 @@ async function processWithdrawalGroupPendingReady(
let redenomRequired = false;
- await wex.db.runReadOnlyTx(["planchets"], async (tx) => {
+ await wex.db.runReadOnlyTx({ storeNames: ["planchets"] }, async (tx) => {
const planchets =
await tx.planchets.indexes.byGroup.getAll(withdrawalGroupId);
for (const p of planchets) {
@@ -1855,7 +1982,7 @@ export async function processWithdrawalGroup(
): Promise<TaskRunResult> {
logger.trace("processing withdrawal group", withdrawalGroupId);
const withdrawalGroup = await wex.db.runReadOnlyTx(
- ["withdrawalGroups"],
+ { storeNames: ["withdrawalGroups"] },
async (tx) => {
return tx.withdrawalGroups.get(withdrawalGroupId);
},
@@ -1865,6 +1992,8 @@ export async function processWithdrawalGroup(
throw Error(`withdrawal group ${withdrawalGroupId} not found`);
}
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+
switch (withdrawalGroup.status) {
case WithdrawalGroupStatus.PendingRegisteringBank:
return await processBankRegisterReserve(wex, withdrawalGroupId);
@@ -1882,6 +2011,8 @@ export async function processWithdrawalGroup(
return await processWithdrawalGroupPendingReady(wex, withdrawalGroup);
case WithdrawalGroupStatus.AbortingBank:
return await processWithdrawalGroupAbortingBank(wex, withdrawalGroup);
+ case WithdrawalGroupStatus.DialogProposed:
+ return await processWithdrawalGroupDialogProposed(ctx, withdrawalGroup);
case WithdrawalGroupStatus.AbortedBank:
case WithdrawalGroupStatus.AbortedExchange:
case WithdrawalGroupStatus.FailedAbortingBank:
@@ -1894,6 +2025,8 @@ export async function processWithdrawalGroup(
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
case WithdrawalGroupStatus.Done:
case WithdrawalGroupStatus.FailedBankAborted:
+ case WithdrawalGroupStatus.AbortedUserRefused:
+ case WithdrawalGroupStatus.AbortedOtherWallet:
// Nothing to do.
return TaskRunResult.finished();
default:
@@ -1912,7 +2045,11 @@ export async function getExchangeWithdrawalInfo(
ageRestricted: number | undefined,
): Promise<ExchangeWithdrawalDetails> {
logger.trace("updating exchange");
- const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
+ const exchange = await fetchFreshExchange(wex, exchangeBaseUrl, {
+ cancellationToken: wex.cancellationToken,
+ });
+
+ wex.cancellationToken.throwIfCancelled();
if (exchange.currency != instructedAmount.currency) {
// Specifying the amount in the conversion input currency is not yet supported.
@@ -1928,22 +2065,28 @@ export async function getExchangeWithdrawalInfo(
exchange,
instructedAmount,
},
- CancellationToken.CONTINUE,
+ wex.cancellationToken,
);
logger.trace("updating withdrawal denoms");
await updateWithdrawalDenoms(wex, exchangeBaseUrl);
+ wex.cancellationToken.throwIfCancelled();
+
logger.trace("getting candidate denoms");
- const denoms = await getCandidateWithdrawalDenoms(
+ const candidateDenoms = await getCandidateWithdrawalDenoms(
wex,
exchangeBaseUrl,
instructedAmount.currency,
);
+
+ wex.cancellationToken.throwIfCancelled();
+
logger.trace("selecting withdrawal denoms");
+ // FIXME: Why not in a transaction?
const selectedDenoms = selectWithdrawalDenominations(
instructedAmount,
- denoms,
+ candidateDenoms,
wex.ws.config.testing.denomselAllowLate,
);
@@ -1963,48 +2106,6 @@ export async function getExchangeWithdrawalInfo(
exchangeWireAccounts.push(account.payto_uri);
}
- let hasDenomWithAgeRestriction = false;
-
- logger.trace("computing earliest deposit expiration");
-
- let earliestDepositExpiration: TalerProtocolTimestamp | undefined;
-
- await wex.db.runReadOnlyTx(["denominations"], async (tx) => {
- for (let i = 0; i < selectedDenoms.selectedDenoms.length; i++) {
- const ds = selectedDenoms.selectedDenoms[i];
- const denom = await getDenomInfo(
- wex,
- tx,
- exchangeBaseUrl,
- ds.denomPubHash,
- );
- checkDbInvariant(!!denom);
- hasDenomWithAgeRestriction =
- hasDenomWithAgeRestriction || denom.denomPub.age_mask > 0;
- const expireDeposit = denom.stampExpireDeposit;
- if (!earliestDepositExpiration) {
- earliestDepositExpiration = expireDeposit;
- continue;
- }
- if (
- AbsoluteTime.cmp(
- AbsoluteTime.fromProtocolTimestamp(expireDeposit),
- AbsoluteTime.fromProtocolTimestamp(earliestDepositExpiration),
- ) < 0
- ) {
- earliestDepositExpiration = expireDeposit;
- }
- }
- });
-
- checkLogicInvariant(!!earliestDepositExpiration);
-
- const possibleDenoms = await getCandidateWithdrawalDenoms(
- wex,
- exchangeBaseUrl,
- instructedAmount.currency,
- );
-
let versionMatch;
if (exchange.protocolVersionRange) {
versionMatch = LibtoolVersion.compare(
@@ -2037,15 +2138,12 @@ export async function getExchangeWithdrawalInfo(
}
const ret: ExchangeWithdrawalDetails = {
- earliestDepositExpiration,
+ earliestDepositExpiration: selectedDenoms.earliestDepositExpiration,
exchangePaytoUris: paytoUris,
exchangeWireAccounts,
exchangeCreditAccountDetails: withdrawalAccountsList,
exchangeVersion: exchange.protocolVersionRange || "unknown",
- numOfferedDenoms: possibleDenoms.length,
selectedDenoms,
- // FIXME: delete this field / replace by something we can display to the user
- trustedAuditorPubs: [],
versionMatch,
walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
termsOfServiceAccepted: tosAccepted,
@@ -2053,7 +2151,7 @@ export async function getExchangeWithdrawalInfo(
withdrawalAmountRaw: Amounts.stringify(instructedAmount),
// TODO: remove hardcoding, this should be calculated from the denominations info
// force enabled for testing
- ageRestrictionOptions: hasDenomWithAgeRestriction
+ ageRestrictionOptions: selectedDenoms.hasDenomWithAgeRestriction
? AGE_MASK_GROUPS
: undefined,
scopeInfo: exchange.scopeInfo,
@@ -2066,12 +2164,6 @@ export interface GetWithdrawalDetailsForUriOpts {
notifyChangeFromPendingTimeoutMs?: number;
}
-type WithdrawalOperationMemoryMap = {
- [uri: string]: boolean | undefined;
-};
-
-const ongoingChecks: WithdrawalOperationMemoryMap = {};
-
/**
* Get more information about a taler://withdraw URI.
*
@@ -2112,37 +2204,6 @@ export async function getWithdrawalDetailsForUri(
);
});
- // FIXME: this should be removed after the extended version of
- // withdrawal state machine. issue #8099
- if (
- info.status === "pending" &&
- opts.notifyChangeFromPendingTimeoutMs !== undefined &&
- !ongoingChecks[talerWithdrawUri]
- ) {
- ongoingChecks[talerWithdrawUri] = true;
- const bankApi = new TalerBankIntegrationHttpClient(
- info.apiBaseUrl,
- wex.http,
- );
-
- bankApi
- .getWithdrawalOperationById(info.operationId, {
- old_state: "pending",
- timeoutMs: opts.notifyChangeFromPendingTimeoutMs,
- })
- .then((resp) => {
- if (resp.type === "ok" && resp.body.status !== "pending") {
- wex.ws.notify({
- type: NotificationType.WithdrawalOperationTransition,
- uri: talerWithdrawUri,
- });
- }
- })
- .finally(() => {
- ongoingChecks[talerWithdrawUri] = false;
- });
- }
-
return {
operationId: info.operationId,
confirmTransferUrl: info.confirmTransferUrl,
@@ -2206,9 +2267,12 @@ async function getWithdrawalGroupRecordTx(
withdrawalGroupId: string;
},
): Promise<WithdrawalGroupRecord | undefined> {
- return await db.runReadOnlyTx(["withdrawalGroups"], async (tx) => {
- return tx.withdrawalGroups.get(req.withdrawalGroupId);
- });
+ return await db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return tx.withdrawalGroups.get(req.withdrawalGroupId);
+ },
+ );
}
export function getReserveRequestTimeout(r: WithdrawalGroupRecord): Duration {
@@ -2244,7 +2308,7 @@ async function registerReserveWithBank(
withdrawalGroupId: string,
): Promise<void> {
const withdrawalGroup = await wex.db.runReadOnlyTx(
- ["withdrawalGroups"],
+ { storeNames: ["withdrawalGroups"] },
async (tx) => {
return await tx.withdrawalGroups.get(withdrawalGroupId);
},
@@ -2501,7 +2565,7 @@ export async function internalPrepareCreateWithdrawalGroup(
args.reserveKeyPair ?? (await wex.cryptoApi.createEddsaKeypair({}));
const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
const secretSeed = encodeCrock(getRandomBytes(32));
- const canonExchange = canonicalizeBaseUrl(args.exchangeBaseUrl);
+ const exchangeBaseUrl = args.exchangeBaseUrl;
const amount = args.amount;
const currency = Amounts.currencyOf(amount);
@@ -2511,7 +2575,7 @@ export async function internalPrepareCreateWithdrawalGroup(
withdrawalGroupId = args.forcedWithdrawalGroupId;
const wgId = withdrawalGroupId;
const existingWg = await wex.db.runReadOnlyTx(
- ["withdrawalGroups"],
+ { storeNames: ["withdrawalGroups"] },
async (tx) => {
return tx.withdrawalGroups.get(wgId);
},
@@ -2528,10 +2592,10 @@ export async function internalPrepareCreateWithdrawalGroup(
withdrawalGroupId = encodeCrock(getRandomBytes(32));
}
- await updateWithdrawalDenoms(wex, canonExchange);
+ await updateWithdrawalDenoms(wex, exchangeBaseUrl);
const denoms = await getCandidateWithdrawalDenoms(
wex,
- canonExchange,
+ exchangeBaseUrl,
currency,
);
@@ -2556,7 +2620,7 @@ export async function internalPrepareCreateWithdrawalGroup(
const withdrawalGroup: WithdrawalGroupRecord = {
denomSelUid,
denomsSel: initialDenomSel,
- exchangeBaseUrl: canonExchange,
+ exchangeBaseUrl: exchangeBaseUrl,
instructedAmount: Amounts.stringify(amount),
timestampStart: timestampPreciseToDb(now),
rawWithdrawalAmount: initialDenomSel.totalWithdrawCost,
@@ -2572,7 +2636,7 @@ export async function internalPrepareCreateWithdrawalGroup(
wgInfo: args.wgInfo,
};
- await fetchFreshExchange(wex, canonExchange);
+ await fetchFreshExchange(wex, exchangeBaseUrl);
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
@@ -2582,7 +2646,7 @@ export async function internalPrepareCreateWithdrawalGroup(
withdrawalGroup,
transactionId,
creationInfo: {
- canonExchange,
+ canonExchange: exchangeBaseUrl,
amount,
},
};
@@ -2698,14 +2762,16 @@ export async function internalCreateWithdrawalGroup(
prep.withdrawalGroup.withdrawalGroupId,
);
const res = await wex.db.runReadWriteTx(
- [
- "withdrawalGroups",
- "reserves",
- "exchanges",
- "exchangeDetails",
- "transactions",
- "operationRetries",
- ],
+ {
+ storeNames: [
+ "withdrawalGroups",
+ "reserves",
+ "exchanges",
+ "exchangeDetails",
+ "transactions",
+ "operationRetries",
+ ],
+ },
async (tx) => {
const res = await internalPerformCreateWithdrawalGroup(wex, tx, prep);
await updateWithdrawalTransaction(ctx, tx);
@@ -2719,6 +2785,133 @@ export async function internalCreateWithdrawalGroup(
return res.withdrawalGroup;
}
+export async function prepareBankIntegratedWithdrawal(
+ wex: WalletExecutionContext,
+ req: {
+ talerWithdrawUri: string;
+ selectedExchange: string;
+ forcedDenomSel?: ForcedDenomSel;
+ restrictAge?: number;
+ },
+): Promise<PrepareBankIntegratedWithdrawalResponse> {
+ const existingWithdrawalGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
+ req.talerWithdrawUri,
+ );
+ },
+ );
+
+ if (existingWithdrawalGroup) {
+ let url: string | undefined;
+ if (
+ existingWithdrawalGroup.wgInfo.withdrawalType ===
+ WithdrawalRecordType.BankIntegrated
+ ) {
+ url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl;
+ }
+ return {
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: existingWithdrawalGroup.withdrawalGroupId,
+ }),
+ };
+ }
+
+ const selectedExchange = req.selectedExchange;
+ const exchange = await fetchFreshExchange(wex, selectedExchange);
+
+ const withdrawInfo = await getBankWithdrawalInfo(
+ wex.http,
+ req.talerWithdrawUri,
+ );
+ const exchangePaytoUri = await getExchangePaytoUri(
+ wex,
+ selectedExchange,
+ withdrawInfo.wireTypes,
+ );
+
+ const withdrawalAccountList = await fetchWithdrawalAccountInfo(
+ wex,
+ {
+ exchange,
+ instructedAmount: withdrawInfo.amount,
+ },
+ wex.cancellationToken,
+ );
+
+ const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
+ amount: withdrawInfo.amount,
+ exchangeBaseUrl: req.selectedExchange,
+ wgInfo: {
+ withdrawalType: WithdrawalRecordType.BankIntegrated,
+ exchangeCreditAccounts: withdrawalAccountList,
+ bankInfo: {
+ exchangePaytoUri,
+ talerWithdrawUri: req.talerWithdrawUri,
+ confirmUrl: withdrawInfo.confirmTransferUrl,
+ timestampBankConfirmed: undefined,
+ timestampReserveInfoPosted: undefined,
+ },
+ },
+ restrictAge: req.restrictAge,
+ forcedDenomSel: req.forcedDenomSel,
+ reserveStatus: WithdrawalGroupStatus.DialogProposed,
+ });
+
+ const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ return {
+ transactionId: ctx.transactionId,
+ };
+}
+
+export async function confirmWithdrawal(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ const parsedTx = parseTransactionIdentifier(transactionId);
+ if (parsedTx?.tag !== TransactionType.Withdrawal) {
+ throw Error("invalid withdrawal transaction ID");
+ }
+ const withdrawalGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return await tx.withdrawalGroups.get(parsedTx.withdrawalGroupId);
+ },
+ );
+
+ if (!withdrawalGroup) {
+ throw Error("withdrawal group not found");
+ }
+
+ const ctx = new WithdrawTransactionContext(
+ wex,
+ withdrawalGroup.withdrawalGroupId,
+ );
+ ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ switch (rec.status) {
+ case WithdrawalGroupStatus.DialogProposed: {
+ rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
+ return TransitionResult.transition(rec);
+ }
+ default:
+ throw Error("unable to confirm withdrawal in current state");
+ }
+ });
+
+ await wex.taskScheduler.resetTaskRetries(ctx.taskId);
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+}
+
/**
* Accept a bank-integrated withdrawal.
*
@@ -2726,6 +2919,8 @@ export async function internalCreateWithdrawalGroup(
*
* Thus after this call returns, the withdrawal operation can be confirmed
* with the bank.
+ *
+ * @deprecated in favor of prepare/accept
*/
export async function acceptWithdrawalFromUri(
wex: WalletExecutionContext,
@@ -2736,12 +2931,12 @@ export async function acceptWithdrawalFromUri(
restrictAge?: number;
},
): Promise<AcceptWithdrawalResponse> {
- const selectedExchange = canonicalizeBaseUrl(req.selectedExchange);
+ const selectedExchange = req.selectedExchange;
logger.info(
`accepting withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
);
const existingWithdrawalGroup = await wex.db.runReadOnlyTx(
- ["withdrawalGroups"],
+ { storeNames: ["withdrawalGroups"] },
async (tx) => {
return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
req.talerWithdrawUri,
@@ -2767,7 +2962,7 @@ export async function acceptWithdrawalFromUri(
};
}
- await fetchFreshExchange(wex, selectedExchange);
+ const exchange = await fetchFreshExchange(wex, selectedExchange);
const withdrawInfo = await getBankWithdrawalInfo(
wex.http,
req.talerWithdrawUri,
@@ -2778,8 +2973,6 @@ export async function acceptWithdrawalFromUri(
withdrawInfo.wireTypes,
);
- const exchange = await fetchFreshExchange(wex, selectedExchange);
-
const withdrawalAccountList = await fetchWithdrawalAccountInfo(
wex,
{
@@ -2835,7 +3028,7 @@ async function internalWaitWithdrawalRegistered(
): Promise<void> {
while (true) {
const { withdrawalRec, retryRec } = await wex.db.runReadOnlyTx(
- ["withdrawalGroups", "operationRetries"],
+ { storeNames: ["withdrawalGroups", "operationRetries"] },
async (tx) => {
return {
withdrawalRec: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
@@ -3039,6 +3232,7 @@ export async function createManualWithdrawal(
amount: AmountLike;
restrictAge?: number;
forcedDenomSel?: ForcedDenomSel;
+ forceReservePriv?: EddsaPrivateKeyString;
},
): Promise<AcceptManualWithdrawalResult> {
const { exchangeBaseUrl } = req;
@@ -3050,9 +3244,20 @@ export async function createManualWithdrawal(
"manual withdrawal with conversion from foreign currency is not yet supported",
);
}
- const reserveKeyPair: EddsaKeypair = await wex.cryptoApi.createEddsaKeypair(
- {},
- );
+
+ let reserveKeyPair: EddsaKeypair;
+ if (req.forceReservePriv) {
+ const pubResp = await wex.cryptoApi.eddsaGetPublic({
+ priv: req.forceReservePriv,
+ });
+
+ reserveKeyPair = {
+ priv: req.forceReservePriv,
+ pub: pubResp.pub,
+ };
+ } else {
+ reserveKeyPair = await wex.cryptoApi.createEddsaKeypair({});
+ }
const withdrawalAccountsList = await fetchWithdrawalAccountInfo(
wex,
@@ -3083,7 +3288,7 @@ export async function createManualWithdrawal(
);
const exchangePaytoUris = await wex.db.runReadOnlyTx(
- ["withdrawalGroups", "exchanges", "exchangeDetails"],
+ { storeNames: ["withdrawalGroups", "exchanges", "exchangeDetails"] },
async (tx) => {
return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId);
},
@@ -3150,7 +3355,7 @@ async function internalWaitWithdrawalFinal(
// Check if refresh is final
const res = await ctx.wex.db.runReadOnlyTx(
- ["withdrawalGroups", "operationRetries"],
+ { storeNames: ["withdrawalGroups", "operationRetries"] },
async (tx) => {
return {
wg: await tx.withdrawalGroups.get(ctx.withdrawalGroupId),
@@ -3177,3 +3382,71 @@ async function internalWaitWithdrawalFinal(
flag.reset();
}
}
+
+export async function getWithdrawalDetailsForAmount(
+ wex: WalletExecutionContext,
+ cts: CancellationToken.Source,
+ req: GetWithdrawalDetailsForAmountRequest,
+): Promise<WithdrawalDetailsForAmount> {
+ const clientCancelKey = req.clientCancellationId
+ ? `ccid:getWithdrawalDetailsForAmount:${req.clientCancellationId}`
+ : undefined;
+ if (clientCancelKey) {
+ const prevCts = wex.ws.clientCancellationMap.get(clientCancelKey);
+ if (prevCts) {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Cancelling previous key ${clientCancelKey}`,
+ });
+ prevCts.cancel();
+ } else {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `No previous key ${clientCancelKey}`,
+ });
+ }
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Setting clientCancelKey ${clientCancelKey} to ${cts}`,
+ });
+ wex.ws.clientCancellationMap.set(clientCancelKey, cts);
+ }
+ try {
+ return await internalGetWithdrawalDetailsForAmount(wex, req);
+ } finally {
+ wex.oc.observe({
+ type: ObservabilityEventType.Message,
+ contents: `Deleting clientCancelKey ${clientCancelKey} to ${cts}`,
+ });
+ if (clientCancelKey && !cts.token.isCancelled) {
+ wex.ws.clientCancellationMap.delete(clientCancelKey);
+ }
+ }
+}
+
+async function internalGetWithdrawalDetailsForAmount(
+ wex: WalletExecutionContext,
+ req: GetWithdrawalDetailsForAmountRequest,
+): Promise<WithdrawalDetailsForAmount> {
+ const wi = await getExchangeWithdrawalInfo(
+ wex,
+ req.exchangeBaseUrl,
+ Amounts.parseOrThrow(req.amount),
+ req.restrictAge,
+ );
+ let numCoins = 0;
+ for (const x of wi.selectedDenoms.selectedDenoms) {
+ numCoins += x.count;
+ }
+ const resp: WithdrawalDetailsForAmount = {
+ amountRaw: req.amount,
+ amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
+ paytoUris: wi.exchangePaytoUris,
+ tosAccepted: wi.termsOfServiceAccepted,
+ ageRestrictionOptions: wi.ageRestrictionOptions,
+ withdrawalAccountsList: wi.exchangeCreditAccountDetails,
+ numCoins,
+ scopeInfo: wi.scopeInfo,
+ };
+ return resp;
+}
diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json
index dcf10acc6..ee9efafdd 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.10.0",
+ "version": "0.10.7",
"description": "",
"engines": {
"node": ">=0.18.0"
diff --git a/packages/taler-wallet-embedded/src/wallet-qjs.ts b/packages/taler-wallet-embedded/src/wallet-qjs.ts
index 8502c779a..98b73fc44 100644
--- a/packages/taler-wallet-embedded/src/wallet-qjs.ts
+++ b/packages/taler-wallet-embedded/src/wallet-qjs.ts
@@ -98,19 +98,10 @@ class NativeWalletMessageHandler {
const wR = await createNativeWalletHost2(this.walletArgs);
const w = wR.wallet;
this.maybeWallet = w;
- const resp = await w.handleCoreApiRequest(
- "initWallet",
- "native-init",
- {
- config: this.walletConfig
- },
- );
- initResponse = resp.type == "response" ? resp.result : resp.error;
- w.runTaskLoop().catch((e) => {
- logger.error(
- `Error during wallet retry loop: ${e.stack ?? e.toString()}`,
- );
+ const resp = await w.handleCoreApiRequest("initWallet", "native-init", {
+ config: this.walletConfig,
});
+ initResponse = resp.type == "response" ? resp.result : resp.error;
this.wp.resolve(w);
};
@@ -296,9 +287,8 @@ export async function testWithGv() {
merchantBaseUrl: "https://backend.demo.taler.net/",
merchantAuthToken: "secret-token:sandbox",
});
- await w.wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ await w.wallet.client.call(WalletApiOperation.Shutdown, {});
}
export async function testWithFdold() {
@@ -317,9 +307,8 @@ export async function testWithFdold() {
exchangeBaseUrl: "https://exchange.taler.fdold.eu/",
merchantBaseUrl: "https://merchant.taler.fdold.eu/",
});
- await w.wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
+ await w.wallet.client.call(WalletApiOperation.Shutdown, {});
}
export async function testWithLocal(path: string) {
@@ -347,11 +336,9 @@ export async function testWithLocal(path: string) {
merchantBaseUrl: "http://localhost:8083/",
});
console.log("started integration test");
- await w.wallet.runTaskLoop({
- stopWhenDone: true,
- });
+ await w.wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {});
console.log("done with task loop");
- w.wallet.stop();
+ await w.wallet.client.call(WalletApiOperation.Shutdown, {});
console.log("DB stats:", j2s(w.getDbStats()));
}
diff --git a/packages/taler-wallet-webextension/manifest-common.json b/packages/taler-wallet-webextension/manifest-common.json
index 2188a1a02..32bd5267f 100644
--- a/packages/taler-wallet-webextension/manifest-common.json
+++ b/packages/taler-wallet-webextension/manifest-common.json
@@ -2,7 +2,7 @@
"name": "GNU Taler Wallet (git)",
"description": "Privacy preserving and transparent payments",
"author": "GNU Taler Developers",
- "version": "0.10.0",
+ "version": "0.10.7",
"icons": {
"16": "static/img/taler-logo-16.png",
"19": "static/img/taler-logo-19.png",
@@ -14,5 +14,5 @@
"256": "static/img/taler-logo-256.png",
"512": "static/img/taler-logo-512.png"
},
- "version_name": "0.10.0"
+ "version_name": "0.10.7"
}
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
index ffe5580bf..bf063d76e 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.10.0",
+ "version": "0.10.7",
"description": "GNU Taler Wallet browser extension",
"main": "./build/index.js",
"types": "./build/index.d.ts",
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index 78a526997..fe348f7fb 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -34,6 +34,7 @@ import {
} from "./components/styled/index.js";
import { useBackendContext } from "./context/backend.js";
import { useAsyncAsHook } from "./hooks/useAsyncAsHook.js";
+import searchIcon from "./svg/search_24px.inline.svg";
import qrIcon from "./svg/qr_code_24px.inline.svg";
import settingsIcon from "./svg/settings_black_24dp.inline.svg";
import warningIcon from "./svg/warning_24px.inline.svg";
@@ -55,7 +56,7 @@ type PageLocation<DynamicPart extends object> = {
function replaceAll(
pattern: string,
vars: Record<string, string>,
- values: Record<string, any>,
+ values: Record<string, string>,
): string {
let result = pattern;
for (const v in vars) {
@@ -75,16 +76,20 @@ function pageDefinition<T extends object>(pattern: string): PageLocation<T> {
`page definition pattern ${pattern} doesn't have any parameter`,
);
- const vars = patternParams.reduce((prev, cur) => {
- const pName = cur.match(/(\w+)/g);
+ const vars = patternParams.reduce(
+ (prev, cur) => {
+ const pName = cur.match(/(\w+)/g);
- //skip things like :? in the path pattern
- if (!pName || !pName[0]) return prev;
- const name = pName[0];
- return { ...prev, [name]: cur };
- }, {} as Record<string, string>);
+ //skip things like :? in the path pattern
+ if (!pName || !pName[0]) return prev;
+ const name = pName[0];
+ return { ...prev, [name]: cur };
+ },
+ {} as Record<string, string>,
+ );
- const f = (values: T): string => replaceAll(pattern, vars, values ?? {});
+ const f = (values: T): string =>
+ replaceAll(pattern, vars, (values ?? {}) as Record<string, string>);
f.pattern = pattern;
return f;
}
@@ -95,6 +100,9 @@ export const Pages = {
balanceHistory: pageDefinition<{ currency?: string }>(
"/balance/history/:currency?",
),
+ searchHistory: pageDefinition<{ currency?: string }>(
+ "/search/history/:currency?",
+ ),
balanceDeposit: pageDefinition<{ amount: string }>(
"/balance/deposit/:amount",
),
@@ -127,6 +135,8 @@ export const Pages = {
ctaRefund: "/cta/refund",
ctaWithdraw: "/cta/withdraw",
ctaDeposit: "/cta/deposit",
+ ctaExperiment: "/cta/experiment",
+ ctaAddExchange: "/cta/add/exchange",
ctaInvoiceCreate: pageDefinition<{ amount?: string }>(
"/cta/invoice/create/:amount?",
),
@@ -151,7 +161,8 @@ const talerUriActionToPageName: {
[TalerUriAction.Restore]: "ctaRecovery",
[TalerUriAction.PayTemplate]: "ctaPayTemplate",
[TalerUriAction.WithdrawExchange]: "ctaWithdrawManual",
- [TalerUriAction.DevExperiment]: undefined,
+ [TalerUriAction.DevExperiment]: "ctaExperiment",
+ [TalerUriAction.AddExchange]: "ctaAddExchange",
};
export function getPathnameForTalerURI(talerUri: string): string | undefined {
@@ -265,6 +276,13 @@ export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
<div
style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
>
+ <a href={Pages.searchHistory({})}>
+ <SvgIcon
+ title={i18n.str`Search transactions`}
+ dangerouslySetInnerHTML={{ __html: searchIcon }}
+ color="white"
+ />
+ </a>
<a href={Pages.qr}>
<SvgIcon
title={i18n.str`QR Reader and Taler URI`}
diff --git a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
index b8bcaa391..6dd577b88 100644
--- a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
+++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
@@ -15,8 +15,10 @@
*/
import { Amounts, ScopeType, WalletBalance } from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
-import { TableWithRoundRows as TableWithRoundedRows } from "./styled/index.js";
+import { Fragment, VNode, h } from "preact";
+import {
+ TableWithRoundRows as TableWithRoundedRows
+} from "./styled/index.js";
export function BalanceTable({
balances,
@@ -26,32 +28,37 @@ export function BalanceTable({
goToWalletHistory: (currency: string) => void;
}): VNode {
return (
- <TableWithRoundedRows>
- {balances.map((entry, idx) => {
- const av = Amounts.parseOrThrow(entry.available);
+ <Fragment>
+ <TableWithRoundedRows>
+ {balances.map((entry, idx) => {
+ const av = Amounts.parseOrThrow(entry.available);
- return (
- <tr
- key={idx}
- onClick={() => goToWalletHistory(av.currency)}
- style={{ cursor: "pointer" }}
- >
- <td>{av.currency}</td>
- <td
- style={{
- fontSize: "2em",
- textAlign: "right",
- width: "100%",
- }}
+ return (
+ <tr
+ key={idx}
+ onClick={() => goToWalletHistory(av.currency)}
+ style={{ cursor: "pointer" }}
>
- {Amounts.stringifyValue(av, 2)}
- <div style={{ fontSize: "small", color: "grey" }}>
- {entry.scopeInfo.type === ScopeType.Exchange || entry.scopeInfo.type === ScopeType.Auditor ? entry.scopeInfo.url : undefined}
- </div>
- </td>
- </tr>
- );
- })}
- </TableWithRoundedRows>
+ <td>{av.currency}</td>
+ <td
+ style={{
+ fontSize: "2em",
+ textAlign: "right",
+ width: "100%",
+ }}
+ >
+ {Amounts.stringifyValue(av, 2)}
+ <div style={{ fontSize: "small", color: "grey" }}>
+ {entry.scopeInfo.type === ScopeType.Exchange ||
+ entry.scopeInfo.type === ScopeType.Auditor
+ ? entry.scopeInfo.url
+ : undefined}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </TableWithRoundedRows>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
index bf77174df..8b6377fc5 100644
--- a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
+++ b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
@@ -21,11 +21,9 @@ import {
segwitMinAmount,
stringifyPaytoUri,
TranslatedString,
- WithdrawalExchangeAccountDetails
+ WithdrawalExchangeAccountDetails,
} from "@gnu-taler/taler-util";
-import {
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { CopiedIcon, CopyIcon } from "../svg/index.js";
@@ -36,72 +34,94 @@ import { Button } from "../mui/Button.js";
export interface BankDetailsProps {
subject: string;
amount: AmountJson;
- accounts: WithdrawalExchangeAccountDetails[],
+ accounts: WithdrawalExchangeAccountDetails[];
}
export function BankDetailsByPaytoType({
subject,
amount,
- accounts,
+ accounts: unsortedAccounts,
}: BankDetailsProps): VNode {
const { i18n } = useTranslationContext();
- const [index, setIndex] = useState(0)
- // const [currency, setCurrency] = useState(amount.currency)
- if (!accounts.length) {
- return <div>the exchange account list is empty</div>
+ const [index, setIndex] = useState(0);
+
+ if (!unsortedAccounts.length) {
+ return <div>the exchange account list is empty</div>;
}
+
+ const accounts = unsortedAccounts.sort((a, b) => {
+ return (b.priority ?? 0) - (a.priority ?? 0);
+ });
+
const selectedAccount = accounts[index];
- const altCurrency = selectedAccount.currencySpecification?.name
+ const altCurrency = selectedAccount.currencySpecification?.name;
const payto = parsePaytoUri(selectedAccount.paytoUri);
if (!payto) return <Fragment />;
- payto.params["amount"] = altCurrency ? selectedAccount.transferAmount! : Amounts.stringify(amount);
+ payto.params["amount"] = altCurrency
+ ? selectedAccount.transferAmount!
+ : Amounts.stringify(amount);
payto.params["message"] = subject;
-
- function Frame({ title, children }: { title: TranslatedString, children: ComponentChildren }): VNode {
- return <section
- style={{
- textAlign: "left",
- border: "solid 1px black",
- padding: 8,
- borderRadius: 4,
- }}
- >
- <div style={{ display: "flex", width: "100%", justifyContent: "space-between" }}>
- <p style={{ marginTop: 0 }}>
- {title}
- </p>
- <div>
-
+ function Frame({
+ title,
+ children,
+ }: {
+ title: TranslatedString;
+ children: ComponentChildren;
+ }): VNode {
+ return (
+ <section
+ style={{
+ textAlign: "left",
+ border: "solid 1px black",
+ padding: 8,
+ borderRadius: 4,
+ }}
+ >
+ <div
+ style={{
+ display: "flex",
+ width: "100%",
+ justifyContent: "space-between",
+ }}
+ >
+ <p style={{ marginTop: 0 }}>{title}</p>
+ <div></div>
</div>
- </div>
- {children}
+ {children}
- {accounts.length > 1 ?
- <Fragment>
- {accounts.map((ac, acIdx) => {
- return <Button variant={acIdx === index ? "contained" : "outlined"}
- onClick={async () => {
- setIndex(acIdx)
- }}
- >
- <i18n.Translate>Account #{acIdx+1} ({ac.currencySpecification?.name ?? amount.currency})</i18n.Translate>
- </Button>
- })}
+ {accounts.length > 1 ? (
+ <Fragment>
+ {accounts.map((ac, acIdx) => {
+ const accountLabel = ac.bankLabel ?? `Account #${acIdx + 1}`;
+ return (
+ <Button
+ key={acIdx}
+ variant={acIdx === index ? "contained" : "outlined"}
+ onClick={async () => {
+ setIndex(acIdx);
+ }}
+ >
+ {accountLabel} (
+ {ac.currencySpecification?.name ?? amount.currency})
+ </Button>
+ );
+ })}
- {/* <Button variant={currency === altCurrency ? "contained" : "outlined"}
+ {/* <Button variant={currency === altCurrency ? "contained" : "outlined"}
onClick={async () => {
setCurrency(altCurrency)
}}
>
<i18n.Translate>{altCurrency}</i18n.Translate>
</Button> */}
- </Fragment>
- : undefined}
- </section>
+ </Fragment>
+ ) : undefined}
+ </section>
+ );
}
if (payto.isKnown && payto.targetType === "bitcoin") {
@@ -157,7 +177,9 @@ export function BankDetailsByPaytoType({
}
const accountPart = !payto.isKnown ? (
- <Row name={i18n.str`Account`} value={payto.targetPath} />
+ <Fragment>
+ <Row name={i18n.str`Account`} value={payto.targetPath} />
+ </Fragment>
) : payto.targetType === "x-taler-bank" ? (
<Fragment>
<Row name={i18n.str`Bank host`} value={payto.host} />
@@ -172,30 +194,68 @@ export function BankDetailsByPaytoType({
</Fragment>
) : undefined;
- const receiver = payto.params["receiver-name"] || payto.params["receiver"] || undefined;
+ const receiver =
+ payto.params["receiver-name"] || payto.params["receiver"] || undefined;
return (
<Frame title={i18n.str`Bank transfer details`}>
<table>
<tbody>
- {accountPart}
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 1:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Copy this code and paste it into the subject/purpose field in
+ your banking app or bank website
+ </i18n.Translate>
+ </td>
+ </tr>
<Row name={i18n.str`Subject`} value={subject} literal />
- <Row
- name={i18n.str`Amount`}
- value={<Amount value={altCurrency ? selectedAccount.transferAmount! : amount} hideCurrency />}
- />
-
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 2:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ If you don't already have it in your banking favourites list,
+ then copy and paste this IBAN and the name into the receiver
+ fields in your banking app or website
+ </i18n.Translate>
+ </td>
+ </tr>
+ {accountPart}
{receiver ? (
<Row name={i18n.str`Receiver name`} value={receiver} />
) : undefined}
<tr>
<td colSpan={3}>
+ <i18n.Translate>Step 3:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Finish the wire transfer setting the amount in your banking app
+ or website, then this withdrawal will proceed automatically.
+ </i18n.Translate>
+ </td>
+ </tr>
+ <Row
+ name={i18n.str`Amount`}
+ value={
+ <Amount
+ value={altCurrency ? selectedAccount.transferAmount! : amount}
+ hideCurrency
+ />
+ }
+ />
+
+ <tr>
+ <td colSpan={3}>
<WarningBox style={{ margin: 0 }}>
<span>
<i18n.Translate>
- Make sure ALL data is correct, including the subject; otherwise, the money will not
- arrive in this wallet. You can use the copy buttons (<CopyIcon />) to prevent typing errors
+ Make sure ALL data is correct, including the subject;
+ otherwise, the money will not arrive in this wallet. You can
+ use the copy buttons (<CopyIcon />) to prevent typing errors
or the "payto://" URI below to copy just one value.
</i18n.Translate>
</span>
@@ -204,22 +264,20 @@ export function BankDetailsByPaytoType({
</tr>
<tr>
- <td>
- <pre>
- <b>
- <a
- target="_bank"
- rel="noreferrer"
- title="RFC 8905 for designating targets for payments"
- href="https://tools.ietf.org/html/rfc8905"
- >
- Payto URI
- </a>
- </b>
- </pre>
- </td>
- <td width="100%" style={{ wordBreak: "break-all" }}>
- {stringifyPaytoUri(payto)}
+ <td colSpan={2} width="100%" style={{ wordBreak: "break-all" }}>
+ <i18n.Translate>
+ Alternative if your bank already supports PayTo URI, you can use
+ this{" "}
+ <a
+ target="_bank"
+ rel="noreferrer"
+ title="RFC 8905 for designating targets for payments"
+ href="https://tools.ietf.org/html/rfc8905"
+ >
+ PayTo URI
+ </a>{" "}
+ link instead
+ </i18n.Translate>
</td>
<td>
<CopyButton getContent={() => stringifyPaytoUri(payto)} />
diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
index 4b44365ea..9be9326b2 100644
--- a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
+++ b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
@@ -23,6 +23,8 @@ import {
TransactionType,
WithdrawalType,
TransactionMajorState,
+ DenomLossEventType,
+ parsePaytoUri,
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -134,10 +136,6 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
}
/>
);
- case TransactionType.Reward:
- return (
- <div>not supported</div>
- );
case TransactionType.Refresh:
return (
<Layout
@@ -155,13 +153,16 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
}
/>
);
- case TransactionType.Deposit:
+ case TransactionType.Deposit:{
+ const payto = parsePaytoUri(tx.targetPaytoUri);
+ const title = payto === undefined || !payto.isKnown ? tx.targetPaytoUri :
+ payto.params["receiver-name"] ;
return (
<Layout
id={tx.transactionId}
amount={tx.amountEffective}
debitCreditIndicator={"debit"}
- title={tx.targetPaytoUri}
+ title={title}
timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
iconPath={"D"}
currentState={tx.txState.major}
@@ -172,6 +173,7 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
}
/>
);
+ }
case TransactionType.PeerPullCredit:
return (
<Layout
@@ -240,6 +242,56 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
}
/>
);
+ case TransactionType.DenomLoss: {
+ switch (tx.lossEventType) {
+ case DenomLossEventType.DenomExpired: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination expired`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ case DenomLossEventType.DenomVanished: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination vanished`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ case DenomLossEventType.DenomUnoffered: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination unoffered`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ default: {
+ assertUnreachable(tx.lossEventType);
+ }
+ }
+ break;
+ }
case TransactionType.Recoup:
throw Error("recoup transaction not implemented");
default: {
@@ -256,12 +308,12 @@ 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
+ props.currentState === TransactionMajorState.Aborting
? "#00000010"
: "inherit",
alignItems: "center",
diff --git a/packages/taler-wallet-webextension/src/components/Modal.tsx b/packages/taler-wallet-webextension/src/components/Modal.tsx
index 5553c72df..f8c0f1651 100644
--- a/packages/taler-wallet-webextension/src/components/Modal.tsx
+++ b/packages/taler-wallet-webextension/src/components/Modal.tsx
@@ -52,7 +52,7 @@ const Body = styled.div`
export function Modal({ title, children, onClose }: Props): VNode {
return (
- <div style={{ position: "fixed", top: 0, width: "100%", height: "100%" }}>
+ <div style={{ top: 0, width: "100%", height: "100%" }}>
<FullSize onClick={onClose?.onClick}>
<div
@@ -64,6 +64,7 @@ export function Modal({ title, children, onClose }: Props): VNode {
margin: "auto",
borderRadius: 8,
padding: 8,
+ zIndex: 100,
// overflow: "scroll",
}}
>
diff --git a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
index 372ca7cb7..c94010ede 100644
--- a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
+++ b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
@@ -42,7 +42,10 @@ interface Props extends JSX.HTMLAttributes {
*/
const cache = { tx: [] as Transaction[] };
-export function PendingTransactions({ goToTransaction, goToURL }: Props): VNode {
+export function PendingTransactions({
+ goToTransaction,
+ goToURL,
+}: Props): VNode {
const api = useBackendContext();
const state = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.GetTransactions, {}),
@@ -59,8 +62,8 @@ export function PendingTransactions({ goToTransaction, goToURL }: Props): VNode
!state || state.hasError
? cache.tx
: state.response.transactions.filter(
- (t) => t.txState.major === TransactionMajorState.Pending,
- );
+ (t) => t.txState.major === TransactionMajorState.Pending,
+ );
if (state && !state.hasError) {
cache.tx = transactions;
@@ -87,50 +90,52 @@ export function PendingTransactionsView({
transactions: Transaction[];
}): VNode {
const { i18n } = useTranslationContext();
- const kycTransaction = transactions.find(tx => tx.kycUrl)
+ const kycTransaction = transactions.find((tx) => tx.kycUrl);
if (kycTransaction) {
- return <div
- style={{
- backgroundColor: "lightcyan",
- display: "flex",
- justifyContent: "center",
- }}
- >
- <Banner
- titleHead={i18n.str`KYC requirement`}
+ return (
+ <div
style={{
- backgroundColor: "lightred",
- maxHeight: 150,
- padding: 8,
- flexGrow: 1,
- maxWidth: 500,
- overflowY: transactions.length > 3 ? "scroll" : "hidden",
+ backgroundColor: "#fff3cd",
+ color: "#664d03",
+ display: "flex",
+ justifyContent: "center",
}}
>
- <Grid
- container
- item
- xs={1}
- wrap="nowrap"
- role="button"
- spacing={1}
- alignItems="center"
- onClick={() => {
- goToURL(kycTransaction.kycUrl ?? "#")
+ <Banner
+ titleHead={i18n.str`KYC requirement`}
+ style={{
+ backgroundColor: "lightred",
+ maxHeight: 150,
+ padding: 8,
+ flexGrow: 1, //#fff3cd //#ffecb5
+ maxWidth: 500,
+ overflowY: transactions.length > 3 ? "scroll" : "hidden",
}}
>
- <Grid item>
- <Typography inline bold>
- One or more transaction require a KYC step to complete
- </Typography>
+ <Grid
+ container
+ item
+ xs={1}
+ wrap="nowrap"
+ role="button"
+ spacing={1}
+ alignItems="center"
+ onClick={() => {
+ goToURL(kycTransaction.kycUrl ?? "#");
+ }}
+ >
+ <Grid item>
+ <Typography inline bold>
+ One or more transaction require a KYC step to complete
+ </Typography>
+ </Grid>
</Grid>
-
- </Grid>
- </Banner>
- </div>
+ </Banner>
+ </div>
+ );
}
- if (!goToTransaction) return <Fragment />
+ if (!goToTransaction) return <Fragment />;
return (
<div
diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
index 99e2d0a76..0e23d5850 100644
--- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
@@ -81,7 +81,7 @@ export const ShowingSimpleOrder = tests.createExample(ShowView, {
contractTerms: cd,
});
export const Error = tests.createExample(ErrorView, {
- proposalId: "asd",
+ transactionId: "asd",
error: {
hasError: true,
message: "message",
diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
index b0f43d0d9..e655def39 100644
--- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
@@ -17,6 +17,7 @@ import {
AbsoluteTime,
Duration,
Location,
+ TransactionIdStr,
WalletContractData,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -83,7 +84,7 @@ export namespace States {
}
export interface Error {
status: "error";
- proposalId: string;
+ transactionId: string;
error: HookError;
hideHandler: ButtonHandler;
}
@@ -99,17 +100,17 @@ export namespace States {
}
interface Props {
- proposalId: string;
+ transactionId: TransactionIdStr;
}
-function useComponentState({ proposalId }: Props): State {
+function useComponentState({ transactionId }: Props): State {
const api = useBackendContext();
const [show, setShow] = useState(false);
const { pushAlertOnError } = useAlertContext();
const hook = useAsyncAsHook(async () => {
if (!show) return undefined;
return await api.wallet.call(WalletApiOperation.GetContractTermsDetails, {
- proposalId,
+ transactionId,
});
}, [show]);
@@ -127,7 +128,7 @@ function useComponentState({ proposalId }: Props): State {
}
if (!hook) return { status: "loading", hideHandler };
if (hook.hasError)
- return { status: "error", proposalId, error: hook, hideHandler };
+ return { status: "error", transactionId, error: hook, hideHandler };
if (!hook.response) return { status: "loading", hideHandler };
return {
status: "show",
@@ -160,7 +161,7 @@ export function LoadingView({ hideHandler }: States.Loading): VNode {
export function ErrorView({
hideHandler,
error,
- proposalId,
+ transactionId,
}: States.Error): VNode {
const { i18n } = useTranslationContext();
return (
@@ -170,7 +171,7 @@ export function ErrorView({
i18n,
i18n.str`Could not load purchase proposal details`,
error,
- { proposalId },
+ { transactionId },
)}
/>
</Modal>
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
index 60839e1f0..41b0c5c76 100644
--- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
+++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
@@ -15,6 +15,7 @@
*/
import {
AbsoluteTime,
+ ExchangeStateTransitionNotification,
NotificationType,
ObservabilityEventType,
RequestProgressNotification,
@@ -22,809 +23,1029 @@ import {
TalerErrorDetail,
TaskProgressNotification,
WalletNotification,
- assertUnreachable
+ assertUnreachable,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, JSX, VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { Pages } from "../NavigationBar.js";
import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { useSettings } from "../hooks/useSettings.js";
import { Button } from "../mui/Button.js";
+import { SafeHandler } from "../mui/handlers.js";
import { WxApiType } from "../wxApi.js";
import { Modal } from "./Modal.js";
import { Time } from "./Time.js";
+import { TextField } from "../mui/TextField.js";
+import { WalletActivityTrack } from "../wxBackend.js";
-interface Props extends JSX.HTMLAttributes {
-}
+const OPEN_ACTIVITY_HEIGHT_PX = 250;
+const CLOSE_ACTIVITY_HEIGHT_PX = 40;
+
+export function WalletActivity(): VNode {
+ const { i18n } = useTranslationContext();
+ const [, updateSettings] = useSettings();
+
+ const [collapsed, setCollcapsed] = useState(true);
-export function WalletActivity({ }: Props): VNode {
- const { i18n } = useTranslationContext()
- const [settings, updateSettings] = useSettings()
- const api = useBackendContext();
useEffect(() => {
- document.body.style.marginBottom = "250px"
+ document.body.style.marginBottom = `${
+ collapsed ? CLOSE_ACTIVITY_HEIGHT_PX : OPEN_ACTIVITY_HEIGHT_PX
+ }px`;
return () => {
- document.body.style.marginBottom = "0px"
- }
- })
- const [table, setTable] = useState<"tasks" | "events">("tasks")
- return (
- <div style={{ position: "fixed", bottom: 0, background: "white", zIndex: 1, height: 250, overflowY: "scroll", width: "100%" }}>
- <div style={{ display: "flex", justifyContent: "space-between", float: "right" }}>
- <div />
- <div>
- <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => {
- updateSettings("showWalletActivity", false)
- }}>
- close
- </div>
+ document.body.style.marginBottom = "0px";
+ };
+ }, [collapsed]);
+
+ const [table, setTable] = useState<"tasks" | "events">("events");
+ if (collapsed) {
+ return (
+ <div
+ style={{
+ position: "fixed",
+ bottom: 0,
+ background: "lightgrey",
+ zIndex: 1,
+ height: CLOSE_ACTIVITY_HEIGHT_PX,
+ overflowY: "scroll",
+ width: "100%",
+ }}
+ onClick={() => {
+ setCollcapsed(!collapsed);
+ }}
+ >
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-around",
+ marginTop: 10,
+ cursor: "pointer",
+ }}
+ >
+ click here to open
</div>
</div>
- <div style={{ display: "flex", justifyContent: "space-around" }}>
- <Button variant={table === "tasks" ? "contained" : "outlined"}
+ );
+ }
+ return (
+ <div
+ style={{
+ position: "fixed",
+ bottom: 0,
+ background: "lightgrey",
+ zIndex: 1,
+ height: OPEN_ACTIVITY_HEIGHT_PX,
+ overflowY: "scroll",
+ width: "100%",
+ }}
+ >
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-around",
+ cursor: "pointer",
+ }}
+ onClick={() => {
+ setCollcapsed(!collapsed);
+ }}
+ >
+ <Button
+ variant={table === "events" ? "contained" : "outlined"}
style={{ margin: 4 }}
onClick={async () => {
- setTable("tasks")
+ setTable("events");
}}
>
- <i18n.Translate>Tasks</i18n.Translate>
+ <i18n.Translate>Events</i18n.Translate>
</Button>
- <Button variant={table === "events" ? "contained" : "outlined"}
+ <Button
+ variant={table === "tasks" ? "contained" : "outlined"}
style={{ margin: 4 }}
onClick={async () => {
- setTable("events")
+ setTable("tasks");
}}
>
- <i18n.Translate>Events</i18n.Translate>
+ <i18n.Translate>Active tasks</i18n.Translate>
</Button>
+ <Button
+ variant="outlined"
+ style={{ margin: 4 }}
+ onClick={async () => {
+ updateSettings("showWalletActivity", false);
+ }}
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </Button>
</div>
- {(function (): VNode {
- switch (table) {
- case "events": {
- return <ObservabilityEventsTable />
- }
- case "tasks": {
- return <ActiveTasksTable />
+ <div
+ style={{
+ backgroundColor: "white",
+ }}
+ >
+ {(function (): VNode {
+ switch (table) {
+ case "events": {
+ return <ObservabilityEventsTable />;
+ }
+ case "tasks": {
+ return <ActiveTasksTable />;
+ }
+ default: {
+ assertUnreachable(table);
+ }
}
- default: {
- assertUnreachable(table)
- }
- }
- })()}
+ })()}
+ </div>
</div>
);
}
-interface MoreInfoPRops { events: (WalletNotification & { when: AbsoluteTime })[], onClick: (content: VNode) => void }
-type Notif = {
- id: string;
+interface MoreInfoPRops {
events: (WalletNotification & { when: AbsoluteTime })[];
- description: string;
- start: AbsoluteTime;
- end: AbsoluteTime;
- reference: {
- eventType: NotificationType,
- referenceType: "task" | "transaction" | "operation" | "exchange",
- id: string;
- } | undefined,
- MoreInfo: (p: MoreInfoPRops) => VNode;
+ onClick: (content: VNode) => void;
}
function ShowBalanceChange({ events }: MoreInfoPRops): VNode {
if (!events.length) return <Fragment />;
const not = events[0];
if (not.type !== NotificationType.BalanceChange) return <Fragment />;
- return <Fragment>
- <dt>Transaction</dt>
- <dd>
- <a title={not.hintTransactionId} href={Pages.balanceTransaction({ tid: not.hintTransactionId })}>{not.hintTransactionId.substring(0, 10)}</a>
- </dd>
- </Fragment>
+ return (
+ <Fragment>
+ <dt>Transaction</dt>
+ <dd>
+ <a
+ title={not.hintTransactionId}
+ href={Pages.balanceTransaction({ tid: not.hintTransactionId })}
+ >
+ {not.hintTransactionId.substring(0, 10)}
+ </a>
+ </dd>
+ </Fragment>
+ );
}
function ShowBackupOperationError({ events, onClick }: MoreInfoPRops): VNode {
if (!events.length) return <Fragment />;
const not = events[0];
if (not.type !== NotificationType.BackupOperationError) return <Fragment />;
- return <Fragment>
- <dt>Error</dt>
- <dd>
- <a href="#" onClick={(e) => {
- e.preventDefault();
- const error = not.error
- onClick(<Fragment>
- <dl>
- <dt>Code</dt>
- <dd>{TalerErrorCode[error.code]} ({error.code})</dd>
- <dt>Hint</dt>
- <dd>{error.hint ?? "--"}</dd>
- <dt>Time</dt>
- <dd><Time
- timestamp={error.when}
- format="yyyy/MM/dd HH:mm:ss"
- /></dd>
- </dl>
- <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
- {JSON.stringify(error, undefined, 2)}
- </pre>
- </Fragment>)
- }}>{TalerErrorCode[not.error.code]}</a>
- </dd>
- </Fragment>
-}
-
-function ShowTransactionStateTransition({ events, onClick }: MoreInfoPRops): VNode {
- if (!events.length) return <Fragment />;
- const not = events[0];
- if (not.type !== NotificationType.TransactionStateTransition) return <Fragment />;
- return <Fragment>
- <dt>Old state</dt>
- <dd>
- {not.oldTxState.major} - {not.oldTxState.minor ?? ""}
- </dd>
- <dt>New state</dt>
- <dd>
- {not.newTxState.major} - {not.newTxState.minor ?? ""}
- </dd>
- <dt>Transaction</dt>
- <dd>
- <a title={not.transactionId} href={Pages.balanceTransaction({ tid: not.transactionId })}>{not.transactionId.substring(0, 10)}</a>
- </dd>
- {not.errorInfo ? <Fragment>
+ return (
+ <Fragment>
<dt>Error</dt>
<dd>
- <a href="#" onClick={(e) => {
- if (!not.errorInfo) return;
- e.preventDefault();
- const error = not.errorInfo;
- onClick(<Fragment>
- <dl>
- <dt>Code</dt>
- <dd>{TalerErrorCode[error.code]} ({error.code})</dd>
- <dt>Hint</dt>
- <dd>{error.hint ?? "--"}</dd>
- <dt>Message</dt>
- <dd>{error.message ?? "--"}</dd>
- </dl>
- </Fragment>)
-
- }}>{TalerErrorCode[not.errorInfo.code]}</a>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ const error = not.error;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" />
+ </dd>
+ </dl>
+ <pre
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {TalerErrorCode[not.error.code]}
+ </a>
</dd>
- </Fragment> : undefined}
- <dt>Experimental</dt>
- <dd>
- <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
- {JSON.stringify(not.experimentalUserData, undefined, 2)}
- </pre>
- </dd>
-
-
- </Fragment>
+ </Fragment>
+ );
}
-function ShowExchangeStateTransition({ events, onClick }: MoreInfoPRops): VNode {
+
+function ShowTransactionStateTransition({
+ events,
+ onClick,
+}: MoreInfoPRops): VNode {
if (!events.length) return <Fragment />;
const not = events[0];
- if (not.type !== NotificationType.ExchangeStateTransition) return <Fragment />;
- return <Fragment>
- <dt>Exchange</dt>
- <dd>
- {not.exchangeBaseUrl}
- </dd>
- {not.oldExchangeState && not.newExchangeState.exchangeEntryStatus !== not.oldExchangeState?.exchangeEntryStatus && <Fragment>
- <dt>Entry status</dt>
+ if (not.type !== NotificationType.TransactionStateTransition)
+ return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Old state</dt>
<dd>
- from {not.oldExchangeState.exchangeEntryStatus} to {not.newExchangeState.exchangeEntryStatus}
+ {not.oldTxState.major} - {not.oldTxState.minor ?? ""}
</dd>
- </Fragment>}
- {not.oldExchangeState && not.newExchangeState.exchangeUpdateStatus !== not.oldExchangeState?.exchangeUpdateStatus && <Fragment>
- <dt>Update status</dt>
+ <dt>New state</dt>
<dd>
- from {not.oldExchangeState.exchangeUpdateStatus} to {not.newExchangeState.exchangeUpdateStatus}
+ {not.newTxState.major} - {not.newTxState.minor ?? ""}
</dd>
- </Fragment>}
- {not.oldExchangeState && not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && <Fragment>
- <dt>Tos status</dt>
+ <dt>Transaction</dt>
+ <dd>
+ <a
+ title={not.transactionId}
+ href={Pages.balanceTransaction({ tid: not.transactionId })}
+ >
+ {not.transactionId.substring(0, 10)}
+ </a>
+ </dd>
+ {not.errorInfo ? (
+ <Fragment>
+ <dt>Error</dt>
+ <dd>
+ <a
+ href="#"
+ onClick={(e) => {
+ if (!not.errorInfo) return;
+ e.preventDefault();
+ const error = not.errorInfo;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Message</dt>
+ <dd>{error.message ?? "--"}</dd>
+ </dl>
+ </Fragment>,
+ );
+ }}
+ >
+ {TalerErrorCode[not.errorInfo.code]}
+ </a>
+ </dd>
+ </Fragment>
+ ) : undefined}
+ <dt>Experimental</dt>
<dd>
- from {not.oldExchangeState.tosStatus} to {not.newExchangeState.tosStatus}
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(not.experimentalUserData, undefined, 2)}
+ </pre>
</dd>
- </Fragment>}
- </Fragment>
+ </Fragment>
+ );
+}
+function ShowExchangeStateTransition({ events }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.ExchangeStateTransition)
+ return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Exchange</dt>
+ <dd>{not.exchangeBaseUrl}</dd>
+ {not.oldExchangeState &&
+ not.newExchangeState.exchangeEntryStatus !==
+ not.oldExchangeState?.exchangeEntryStatus && (
+ <Fragment>
+ <dt>Entry status</dt>
+ <dd>
+ from {not.oldExchangeState.exchangeEntryStatus} to{" "}
+ {not.newExchangeState.exchangeEntryStatus}
+ </dd>
+ </Fragment>
+ )}
+ {not.oldExchangeState &&
+ not.newExchangeState.exchangeUpdateStatus !==
+ not.oldExchangeState?.exchangeUpdateStatus && (
+ <Fragment>
+ <dt>Update status</dt>
+ <dd>
+ from {not.oldExchangeState.exchangeUpdateStatus} to{" "}
+ {not.newExchangeState.exchangeUpdateStatus}
+ </dd>
+ </Fragment>
+ )}
+ {not.oldExchangeState &&
+ not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && (
+ <Fragment>
+ <dt>Tos status</dt>
+ <dd>
+ from {not.oldExchangeState.tosStatus} to{" "}
+ {not.newExchangeState.tosStatus}
+ </dd>
+ </Fragment>
+ )}
+ </Fragment>
+ );
}
-type ObservaNotifWithTime = ((TaskProgressNotification | RequestProgressNotification) & {
+type ObservaNotifWithTime = (
+ | TaskProgressNotification
+ | RequestProgressNotification
+) & {
when: AbsoluteTime;
-})
+};
function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode {
// let prev: ObservaNotifWithTime;
- const asd = events.map(not => {
- if (not.type !== NotificationType.RequestObservabilityEvent && not.type !== NotificationType.TaskObservabilityEvent) return <Fragment />;
+ const asd = events.map((not, idx) => {
+ if (
+ not.type !== NotificationType.RequestObservabilityEvent &&
+ not.type !== NotificationType.TaskObservabilityEvent
+ )
+ return <Fragment />;
const title = (function () {
switch (not.event.type) {
case ObservabilityEventType.HttpFetchFinishError:
case ObservabilityEventType.HttpFetchFinishSuccess:
- case ObservabilityEventType.HttpFetchStart: return "HTTP Request"
+ case ObservabilityEventType.HttpFetchStart:
+ return "HTTP Request";
case ObservabilityEventType.DbQueryFinishSuccess:
case ObservabilityEventType.DbQueryFinishError:
- case ObservabilityEventType.DbQueryStart: return "Database"
+ case ObservabilityEventType.DbQueryStart:
+ return "Database";
case ObservabilityEventType.RequestFinishSuccess:
case ObservabilityEventType.RequestFinishError:
- case ObservabilityEventType.RequestStart: return "Wallet"
+ case ObservabilityEventType.RequestStart:
+ return "Wallet";
case ObservabilityEventType.CryptoFinishSuccess:
case ObservabilityEventType.CryptoFinishError:
- case ObservabilityEventType.CryptoStart: return "Crypto"
- case ObservabilityEventType.TaskStart: return "Task start"
- case ObservabilityEventType.TaskStop: return "Task stop"
- case ObservabilityEventType.TaskReset: return "Task reset"
- case ObservabilityEventType.ShepherdTaskResult: return "Schedule"
- case ObservabilityEventType.DeclareTaskDependency: return "Task dependency"
+ case ObservabilityEventType.CryptoStart:
+ return "Crypto";
+ case ObservabilityEventType.TaskStart:
+ return "Task start";
+ case ObservabilityEventType.TaskStop:
+ return "Task stop";
+ case ObservabilityEventType.TaskReset:
+ return "Task reset";
+ case ObservabilityEventType.ShepherdTaskResult:
+ return "Schedule";
+ case ObservabilityEventType.DeclareTaskDependency:
+ return "Task dependency";
+ case ObservabilityEventType.Message:
+ return "Message";
}
})();
- return <ShowObervavilityDetails title={title} notif={not} onClick={onClick} />
-
- })
- return <table>
- <thead>
- <td>Event</td>
- <td>Info</td>
- <td>Start</td>
- <td>End</td>
- </thead>
- <tbody>
- {asd}
- </tbody>
- </table>
+ return (
+ <ShowObervavilityDetails
+ key={idx}
+ title={title}
+ notif={not}
+ onClick={onClick}
+ />
+ );
+ });
+ return (
+ <table>
+ <thead>
+ <td>Event</td>
+ <td>Info</td>
+ <td>Start</td>
+ <td>End</td>
+ </thead>
+ <tbody>{asd}</tbody>
+ </table>
+ );
}
-function ShowObervavilityDetails({ title, notif, onClick, prev }: { title: string, notif: ObservaNotifWithTime, prev?: ObservaNotifWithTime, onClick: (content: VNode) => void }): VNode {
+function ShowObervavilityDetails({
+ title,
+ notif,
+ onClick,
+ prev,
+}: {
+ title: string;
+ notif: ObservaNotifWithTime;
+ prev?: ObservaNotifWithTime;
+ onClick: (content: VNode) => void;
+}): VNode {
switch (notif.event.type) {
case ObservabilityEventType.HttpFetchStart:
case ObservabilityEventType.HttpFetchFinishError:
case ObservabilityEventType.HttpFetchFinishSuccess: {
- return <tr>
- <td><a href="#" onClick={(e) => {
- e.preventDefault();
- onClick(<Fragment>
- <pre
- style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
- >
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
- </pre>
- </Fragment>);
- }}>{title}</a></td>
- <td>
- {notif.event.url} {
- prev?.event.type === ObservabilityEventType.HttpFetchFinishSuccess ? `(${prev.event.status})`
- : prev?.event.type === ObservabilityEventType.HttpFetchFinishError ? <a href="#" onClick={(e) => {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
e.preventDefault();
- if (prev.event.type !== ObservabilityEventType.HttpFetchFinishError) return;
- const error = prev.event.error
- onClick(<Fragment>
- <dl>
- <dt>Code</dt>
- <dd>{TalerErrorCode[error.code]} ({error.code})</dd>
- <dt>Hint</dt>
- <dd>{error.hint ?? "--"}</dd>
- <dt>Time</dt>
- <dd><Time
- timestamp={error.when}
- format="yyyy/MM/dd HH:mm:ss"
- /></dd>
- </dl>
- <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
- {JSON.stringify(error, undefined, 2)}
- </pre>
-
- </Fragment>)
- }}>fail</a> : undefined
- }
- </td>
- <td> <Time
- timestamp={notif.when}
- format="yyyy/MM/dd HH:mm:ss"
- /></td>
- <td> <Time
- timestamp={prev?.when}
- format="yyyy/MM/dd HH:mm:ss"
- /></td>
- </tr>
-
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>
+ {notif.event.url}{" "}
+ {prev?.event.type ===
+ ObservabilityEventType.HttpFetchFinishSuccess ? (
+ `(${prev.event.status})`
+ ) : prev?.event.type ===
+ ObservabilityEventType.HttpFetchFinishError ? (
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ if (
+ prev.event.type !==
+ ObservabilityEventType.HttpFetchFinishError
+ )
+ return;
+ const error = prev.event.error;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time
+ timestamp={error.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </dd>
+ </dl>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ fail
+ </a>
+ ) : undefined}
+ </td>
+ <td>
+ {" "}
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ {" "}
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
}
case ObservabilityEventType.DbQueryStart:
case ObservabilityEventType.DbQueryFinishSuccess:
case ObservabilityEventType.DbQueryFinishError: {
- return <tr>
- <td><a href="#" onClick={(e) => {
- e.preventDefault();
- onClick(<Fragment>
- <pre
- style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
- </pre>
- </Fragment>);
- }}>{title}</a></td>
- <td>
- {notif.event.location} {notif.event.name}
- </td>
- <td>
- <Time
- timestamp={notif.when}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- <td>
- <Time
- timestamp={prev?.when}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- </tr>
+ {title}
+ </a>
+ </td>
+ <td>
+ {notif.event.location} {notif.event.name}
+ </td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
}
case ObservabilityEventType.TaskStart:
case ObservabilityEventType.TaskStop:
case ObservabilityEventType.DeclareTaskDependency:
case ObservabilityEventType.TaskReset: {
- return <tr>
- <td><a href="#" onClick={(e) => {
- e.preventDefault();
- onClick(<Fragment>
- <pre
- style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
- </pre>
- </Fragment>);
- }}>{title}</a></td>
- <td>
- {notif.event.taskId}
- </td>
- <td>
- <Time
- timestamp={notif.when}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- <td>
- <Time
- timestamp={prev?.when}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- </tr>
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.taskId}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
}
case ObservabilityEventType.ShepherdTaskResult: {
- return <tr>
- <td><a href="#" onClick={(e) => {
- e.preventDefault();
- onClick(<Fragment>
- <pre
- style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
- </pre>
- </Fragment>);
- }}>{title}</a></td>
- <td>
- {notif.event.resultType}
- </td>
- <td>
- <Time
- timestamp={notif.when}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- <td>
- <Time
- timestamp={prev?.when}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- </tr>
-
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.resultType}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
}
case ObservabilityEventType.CryptoStart:
case ObservabilityEventType.CryptoFinishSuccess:
case ObservabilityEventType.CryptoFinishError: {
- return <tr>
- <td><a href="#" onClick={(e) => {
- e.preventDefault();
- onClick(<Fragment>
- <pre
- style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
- </pre>
- </Fragment>);
- }}>{title}</a></td>
- <td>
- {notif.event.operation}
- </td>
- <td>
- <Time
- timestamp={notif.when}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- <td>
- <Time
- timestamp={prev?.when}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- </tr>
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.operation}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
}
case ObservabilityEventType.RequestStart:
case ObservabilityEventType.RequestFinishSuccess:
case ObservabilityEventType.RequestFinishError: {
- return <tr >
- <td><a href="#" onClick={(e) => {
- e.preventDefault();
- onClick(<Fragment>
- <pre
- style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
>
- {JSON.stringify({ event: notif, prev }, undefined, 2)}
- </pre>
- </Fragment>);
- }}>{title}</a></td>
- <td>
- {notif.event.type}
- </td>
- <td>
- <Time
- timestamp={notif.when}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- <td>
- <Time
- timestamp={prev?.when}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- </tr>
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.type}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
}
+ case ObservabilityEventType.Message:
+ // FIXME
+ return <></>;
}
}
-function getNotificationFor(id: string, event: WalletNotification, start: AbsoluteTime, list: Notif[]): Notif | undefined {
- const eventWithTime = { ...event, when: start }
- switch (event.type) {
- case NotificationType.BalanceChange: {
- return ({
- id,
- events: [eventWithTime],
- reference: {
- eventType: event.type,
- referenceType: "transaction",
- id: event.hintTransactionId,
- },
- description: "Balance change",
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowBalanceChange
- })
- }
- case NotificationType.BackupOperationError: {
- return ({
- id,
- events: [eventWithTime],
- reference: undefined,
- description: "Backup error",
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowBackupOperationError
- })
- }
- case NotificationType.TransactionStateTransition: {
- const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.transactionId)
- if (found) {
- found.end = start;
- found.events.unshift(eventWithTime)
- return undefined
- }
- return ({
- id,
- events: [eventWithTime],
- reference: {
- eventType: event.type,
- referenceType: "transaction",
- id: event.transactionId,
- },
- description: event.type,
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowTransactionStateTransition
- })
- }
- case NotificationType.ExchangeStateTransition: {
- const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.exchangeBaseUrl)
- if (found) {
- found.end = start;
- found.events.unshift(eventWithTime)
- return undefined
- }
- return ({
- id,
- events: [eventWithTime],
- description: "Exchange update",
- reference: {
- eventType: event.type,
- referenceType: "exchange",
- id: event.exchangeBaseUrl,
- },
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowExchangeStateTransition
- })
- }
- case NotificationType.TaskObservabilityEvent: {
- const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.taskId)
- if (found) {
- found.end = start;
- found.events.unshift(eventWithTime)
- return undefined
- }
- return ({
- id,
- events: [eventWithTime],
- reference: {
- eventType: event.type,
- referenceType: "task",
- id: event.taskId,
- },
- description: `Task update ${event.taskId}`,
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowObservabilityEvent
- })
- }
- case NotificationType.WithdrawalOperationTransition: {
- const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.uri)
- if (found) {
- found.end = start;
- found.events.unshift(eventWithTime)
- return undefined
- }
- return ({
- id,
- events: [eventWithTime],
- reference: {
- eventType: event.type,
- referenceType: "task",
- id: event.uri,
- },
- description: `Withdrawal operation updated`,
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowObservabilityEvent
- })
- }
- case NotificationType.RequestObservabilityEvent: {
- const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.requestId)
- if (found) {
- found.end = start;
- found.events.unshift(eventWithTime)
- return undefined
- }
- return ({
- id,
- events: [eventWithTime],
- reference: {
- eventType: event.type,
- referenceType: "operation",
- id: event.requestId,
- },
- description: `wallet.${event.operation}(${event.requestId})`,
- start,
- end: AbsoluteTime.never(),
- MoreInfo: ShowObservabilityEvent
- })
- }
- default: {
- assertUnreachable(event)
- }
- }
+function refresh(
+ api: WxApiType,
+ onUpdate: (list: WalletActivityTrack[]) => void,
+ filter: string,
+) {
+ api.background
+ .call("getNotifications", { filter })
+ .then((notif) => {
+ onUpdate(notif);
+ })
+ .catch((error) => {
+ console.log(error);
+ });
}
-
-function refresh(api: WxApiType, onUpdate: (list: Notif[]) => void) {
- api.background.call("getNotifications", undefined).then(notif => {
-
- const list: Notif[] = []
- for (const n of notif) {
- if (n.notification.type === NotificationType.RequestObservabilityEvent &&
- n.notification.operation === "getActiveTasks") {
- //ignore monitor request
- continue;
- }
- const event = getNotificationFor(String(list.length), n.notification, n.when, list)
- // pepe.
- if (event) {
- list.unshift(event)
- }
- }
- onUpdate(list);
- }).catch(error => {
- console.log(error)
- })
-}
-
-export function ObservabilityEventsTable({ }: {}): VNode {
- const { i18n } = useTranslationContext()
+export function ObservabilityEventsTable(): VNode {
+ const { i18n } = useTranslationContext();
const api = useBackendContext();
- const [notifications, setNotifications] = useState<Notif[]>([])
- const [showDetails, setShowDetails] = useState<VNode>()
+ const [notifications, setNotifications] = useState<WalletActivityTrack[]>([]);
+ const [showDetails, setShowDetails] = useState<VNode>();
+ const [filter, onChangeFilter] = useState("");
useEffect(() => {
let lastTimeout: ReturnType<typeof setTimeout>;
function periodicRefresh() {
-
- refresh(api, setNotifications)
+ refresh(api, setNotifications, filter);
lastTimeout = setTimeout(() => {
periodicRefresh();
- }, 1000)
+ }, 1000);
- //clear on unload
- return () => { clearTimeout(lastTimeout) }
+ return () => {
+ clearTimeout(lastTimeout);
+ };
}
- return periodicRefresh()
- }, [1]);
-
- return <div>
- <div style={{ display: "flex", justifyContent: "space-between" }}>
+ return periodicRefresh();
+ }, [filter]);
- <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => {
- api.background.call("clearNotifications", undefined).then(d => {
- refresh(api, setNotifications)
- })
- }}>
- clear
+ return (
+ <div>
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <TextField
+ label="Filter"
+ variant="outlined"
+ value={filter}
+ onChange={onChangeFilter}
+ />
+ <div
+ style={{
+ padding: 4,
+ margin: 2,
+ border: "solid 1px black",
+ alignSelf: "center",
+ }}
+ onClick={() => {
+ api.background.call("clearNotifications", undefined).then(() => {
+ refresh(api, setNotifications, filter);
+ });
+ }}
+ >
+ clear
+ </div>
</div>
-
-
- </div>
- {showDetails && <Modal title="event details" onClose={{ onClick: (async () => { setShowDetails(undefined) }) as any }} >
- {showDetails}
- </Modal>}
- {notifications.map((not) => {
- return (
- <details key={not.id}>
- <summary>
- <div style={{ width: "90%", display: "inline-flex", justifyContent: "space-between", padding: 4 }}>
- <div style={{ padding: 4 }}>
- {not.description}
- </div>
- <div style={{ padding: 4 }}>
- <Time
- timestamp={not.start}
- format="yyyy/MM/dd HH:mm:ss"
- />
+ {showDetails && (
+ <Modal
+ title="event details"
+ onClose={{
+ onClick: (async () => {
+ setShowDetails(undefined);
+ }) as SafeHandler<void>,
+ }}
+ >
+ {showDetails}
+ </Modal>
+ )}
+ {notifications.map((not) => {
+ return (
+ <details key={not.id}>
+ <summary>
+ <div
+ style={{
+ width: "90%",
+ display: "inline-flex",
+ justifyContent: "space-between",
+ padding: 4,
+ }}
+ >
+ <div style={{ padding: 4 }}>
+ {(() => {
+ switch (not.type) {
+ case NotificationType.BalanceChange:
+ return i18n.str`Balance change`;
+ case NotificationType.BackupOperationError:
+ return i18n.str`Backup failed`;
+ case NotificationType.TransactionStateTransition:
+ return i18n.str`Transaction updated`;
+ case NotificationType.ExchangeStateTransition:
+ return i18n.str`Exchange updated`;
+ case NotificationType.Idle:
+ return i18n.str`Idle`;
+ case NotificationType.TaskObservabilityEvent:
+ return i18n.str`task.${
+ (not.events[0] as TaskProgressNotification).taskId
+ }`;
+ case NotificationType.RequestObservabilityEvent:
+ return i18n.str`wallet.${
+ (not.events[0] as RequestProgressNotification)
+ .operation
+ }(${
+ (not.events[0] as RequestProgressNotification)
+ .requestId
+ })`;
+ case NotificationType.WithdrawalOperationTransition: {
+ return `---`;
+ }
+ default: {
+ assertUnreachable(not.type);
+ }
+ }
+ })()}
+ </div>
+ <div style={{ padding: 4 }}>
+ <Time timestamp={not.start} format="yyyy/MM/dd HH:mm:ss" />
+ </div>
+ <div style={{ padding: 4 }}>
+ <Time timestamp={not.end} format="yyyy/MM/dd HH:mm:ss" />
+ </div>
</div>
- <div style={{ padding: 4 }}><Time
- timestamp={not.end}
- format="yyyy/MM/dd HH:mm:ss"
- /></div>
- </div>
- </summary>
- <not.MoreInfo events={not.events} onClick={(details) => {
- setShowDetails(details)
- }} />
- </details>
- );
- })}
- </div >
+ </summary>
+ {(() => {
+ switch (not.type) {
+ case NotificationType.BalanceChange: {
+ return (
+ <ShowBalanceChange
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.BackupOperationError: {
+ return (
+ <ShowBackupOperationError
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.TransactionStateTransition: {
+ return (
+ <ShowTransactionStateTransition
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.ExchangeStateTransition: {
+ return (
+ <ShowExchangeStateTransition
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.Idle: {
+ return <div>not implemented</div>;
+ }
+ case NotificationType.TaskObservabilityEvent: {
+ return (
+ <ShowObservabilityEvent
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.RequestObservabilityEvent: {
+ return (
+ <ShowObservabilityEvent
+ events={not.events}
+ onClick={(details) => {
+ setShowDetails(details);
+ }}
+ />
+ );
+ }
+ case NotificationType.WithdrawalOperationTransition: {
+ return <div>not implemented</div>;
+ }
+ }
+ })()}
+ </details>
+ );
+ })}
+ </div>
+ );
}
-function ErroDetailModal({ error, onClose }: { error: TalerErrorDetail, onClose: () => void }): VNode {
- return <Modal title="Full detail" onClose={{
- onClick: onClose as any
- }}>
- <dl>
- <dt>Code</dt>
- <dd>{TalerErrorCode[error.code]} ({error.code})</dd>
- <dt>Hint</dt>
- <dd>{error.hint ?? "--"}</dd>
- <dt>Time</dt>
- <dd><Time
- timestamp={error.when}
- format="yyyy/MM/dd HH:mm:ss"
- /></dd>
- </dl>
- <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
- {JSON.stringify(error, undefined, 2)}
- </pre>
- </Modal>
+function ErroDetailModal({
+ error,
+ onClose,
+}: {
+ error: TalerErrorDetail;
+ onClose: () => void;
+}): VNode {
+ return (
+ <Modal
+ title="Full detail"
+ onClose={{
+ onClick: onClose as SafeHandler<void>,
+ }}
+ >
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" />
+ </dd>
+ </dl>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Modal>
+ );
}
-export function ActiveTasksTable({ }: {}): VNode {
- const { i18n } = useTranslationContext()
+export function ActiveTasksTable(): VNode {
+ const { i18n } = useTranslationContext();
const api = useBackendContext();
const state = useAsyncAsHook(() => {
return api.wallet.call(WalletApiOperation.GetActiveTasks, {});
});
- const [showError, setShowError] = useState<TalerErrorDetail>()
+ const [showError, setShowError] = useState<TalerErrorDetail>();
const tasks = state && !state.hasError ? state.response.tasks : [];
useEffect(() => {
- if (!state || state.hasError) return
+ if (!state || state.hasError) return;
const lastTimeout = setTimeout(() => {
state.retry();
- }, 1000)
+ }, 1000);
return () => {
- clearTimeout(lastTimeout)
- }
- }, [tasks])
+ clearTimeout(lastTimeout);
+ };
+ }, [tasks]);
- // const listenAllEvents = Array.from<NotificationType>({ length: 1 });
- // listenAllEvents.includes = () => true
- // useEffect(() => {
- // return api.listener.onUpdateNotification(listenAllEvents, (notif) => {
- // state?.retry()
- // });
- // });
- return <Fragment>
- {showError && <ErroDetailModal error={showError} onClose={(async () => { setShowError(undefined) })} />}
+ return (
+ <Fragment>
+ {showError && (
+ <ErroDetailModal
+ error={showError}
+ onClose={async () => {
+ setShowError(undefined);
+ }}
+ />
+ )}
- <table style={{ width: "100%" }}>
- <thead>
- <tr>
- <th>
- <i18n.Translate>Type</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>Id</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>Since</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>Next try</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>Error</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>Transaction</i18n.Translate>
- </th>
- </tr>
- </thead>
- <tbody>
- {tasks.map((task) => {
- const [type, id] = task.id.split(":")
- return (
- <tr>
- <td>{type}</td>
- <td title={id}>{id.substring(0, 10)}</td>
- <td>
- <Time
- timestamp={task.firstTry}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- <td>
- <Time
- timestamp={task.nextTry}
- format="yyyy/MM/dd HH:mm:ss"
- />
- </td>
- <td>{!task.lastError?.code ? "" : <a href="#" onClick={(e) => { e.preventDefault(); setShowError(task.lastError) }}>{TalerErrorCode[task.lastError.code]}</a>}</td>
- <td>
- {task.transaction ? <a title={task.transaction} href={Pages.balanceTransaction({ tid: task.transaction })}>{task.transaction.substring(0, 10)}</a> : "--"}
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- </Fragment>
-} \ No newline at end of file
+ <table style={{ width: "100%" }}>
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Type</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Id</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Since</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Next try</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Error</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Transaction</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {tasks.map((task) => {
+ const [type, id] = task.taskId.split(":");
+ return (
+ <tr key={id}>
+ <td>{type}</td>
+ <td title={id}>{id.substring(0, 10)}</td>
+ <td>
+ <Time
+ timestamp={task.firstTry}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ <td>
+ <Time timestamp={task.nextTry} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ {!task.lastError?.code ? (
+ ""
+ ) : (
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setShowError(task.lastError);
+ }}
+ >
+ {TalerErrorCode[task.lastError.code]}
+ </a>
+ )}
+ </td>
+ <td>
+ {task.transaction ? (
+ <a
+ title={task.transaction}
+ href={Pages.balanceTransaction({ tid: task.transaction })}
+ >
+ {task.transaction.substring(0, 10)}
+ </a>
+ ) : (
+ "--"
+ )}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/context/alert.ts b/packages/taler-wallet-webextension/src/context/alert.ts
index 36de7c7e4..e30fdd72c 100644
--- a/packages/taler-wallet-webextension/src/context/alert.ts
+++ b/packages/taler-wallet-webextension/src/context/alert.ts
@@ -19,13 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerError, TalerErrorCode, TalerErrorDetail, TranslatedString } from "@gnu-taler/taler-util";
+import {
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext, useState } from "preact/hooks";
import { HookError } from "../hooks/useAsyncAsHook.js";
import { SafeHandler, withSafe } from "../mui/handlers.js";
import { BackgroundError } from "../wxApi.js";
-import { InternationalizationAPI, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ InternationalizationAPI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { platform } from "../platform/foreground.js";
export type AlertType = "info" | "warning" | "error" | "success";
@@ -175,9 +183,14 @@ export function alertFromError(
//HookError
description = error.message as TranslatedString;
if (error.type === "taler") {
- const msg = isWalletNotAvailable(i18n,error.details)
+ const msg = isWalletNotAvailable(i18n, error.details);
if (msg) {
- description = msg
+ description = msg;
+ } else {
+ const msg2 = isHttpError(i18n, error.details);
+ if (msg2) {
+ description = msg2;
+ }
}
cause = {
details: error.details,
@@ -185,12 +198,17 @@ export function alertFromError(
}
} else {
if (error instanceof BackgroundError) {
- const msg = isWalletNotAvailable(i18n,error.errorDetail)
+ const msg = isWalletNotAvailable(i18n, error.errorDetail);
if (msg) {
- description = msg
+ description = msg;
} else {
- description = (error.errorDetail.hint ??
- `Error code: ${error.errorDetail.code}`) as TranslatedString;
+ const msg2 = isHttpError(i18n, error.errorDetail);
+ if (msg2) {
+ description = msg2;
+ } else {
+ description = (error.errorDetail.hint ??
+ `Error code: ${error.errorDetail.code}`) as TranslatedString;
+ }
}
cause = {
details: error.errorDetail,
@@ -217,20 +235,43 @@ export function alertFromError(
};
}
-function isWalletNotAvailable(i18n: InternationalizationAPI, detail: TalerErrorDetail): TranslatedString | undefined {
- if (detail.code === TalerErrorCode.WALLET_CORE_NOT_AVAILABLE
- && detail.lastError) {
- const le = detail.lastError as TalerErrorDetail
+function isWalletNotAvailable(
+ i18n: InternationalizationAPI,
+ detail: TalerErrorDetail,
+): TranslatedString | undefined {
+ if (
+ detail.code === TalerErrorCode.WALLET_CORE_NOT_AVAILABLE &&
+ detail.lastError
+ ) {
+ const le = detail.lastError as TalerErrorDetail;
if (le.code === TalerErrorCode.WALLET_DB_UNAVAILABLE) {
if (platform.isFirefox() && platform.runningOnPrivateMode()) {
- return i18n.str`Could not open the wallet database. Firefox is known to run into this problem under "permanent private mode".`
+ return i18n.str`Could not open the wallet database. Firefox is known to run into this problem under "permanent private mode".`;
} else {
- return i18n.str`Could not open the wallet database.`
+ return i18n.str`Could not open the wallet database.`;
}
} else {
return (detail.hint ?? `Error code: ${detail.code}`) as TranslatedString;
}
+ }
+ return undefined;
+}
+function isHttpError(
+ i18n: InternationalizationAPI,
+ detail: TalerErrorDetail,
+): TranslatedString | undefined {
+ if (
+ detail.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
+ detail.errorResponse
+ ) {
+ const er = detail.errorResponse as TalerErrorDetail;
+ return (
+ (er.hint as TranslatedString) ??
+ detail.hint ??
+ i18n.str`Unexpected request error, code: ${er.code}`
+ );
}
- return undefined
+ return undefined;
}
+//
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts
new file mode 100644
index 000000000..ec09fd9f1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts
@@ -0,0 +1,73 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { InsertLostView, InsertPendingRefreshView, UnknownView } from "./views.js";
+
+export interface Props {
+ talerExperimentUri: string | undefined;
+ onCancel: () => Promise<void>;
+ onSuccess: () => Promise<void>;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Unknown | State.InsertLost | State.PendingRefresh;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+ export interface InsertLost {
+ status: "insertLost";
+ error: undefined;
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
+ }
+ export interface PendingRefresh {
+ status: "pendingRefresh";
+ error: undefined;
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
+ }
+ export interface Unknown {
+ status: "unknown";
+ experimentId: string;
+ error: undefined;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ pendingRefresh: InsertPendingRefreshView,
+ insertLost: InsertLostView,
+ unknown: UnknownView,
+};
+
+export const DevExperimentPage = compose(
+ "DevExperiment",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts
new file mode 100644
index 000000000..774a1129d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { parseDevExperimentUri } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerExperimentUri,
+ onCancel,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
+
+ async function doApply(): Promise<void> {
+ if (!talerExperimentUri) return;
+ await api.wallet.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: talerExperimentUri
+ })
+ // const resp = await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
+ // amount: Amounts.stringify(amount),
+ // depositPaytoUri: uri,
+ // });
+ onSuccess();
+ }
+ const uri = talerExperimentUri === undefined ? undefined : parseDevExperimentUri(talerExperimentUri);
+
+ if (!uri) {
+ return {
+ status: "error",
+ error: {
+ type: "error",
+ message: i18n.str`Invalid dev experiment URI.`,
+ description: i18n.str`URI: ${talerExperimentUri}`,
+ cause: {},
+ context: {},
+ },
+ };
+ }
+ if (uri.devExperimentId === "insert-denom-loss") {
+ return {
+ status: "insertLost",
+ error: undefined,
+ confirm: {
+ onClick: pushAlertOnError(doApply),
+ },
+ cancel: onCancel,
+ };
+ }
+ if (uri.devExperimentId === "insert-pending-refresh") {
+ return {
+ status: "pendingRefresh",
+ error: undefined,
+ confirm: {
+ onClick: pushAlertOnError(doApply),
+ },
+ cancel: onCancel,
+ };
+ }
+ return {
+ status: "unknown",
+ error: undefined,
+ experimentId: uri.devExperimentId,
+ }
+}
diff --git a/packages/aml-backoffice-ui/src/settings.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx
index 68f44b4df..c9851495f 100644
--- a/packages/aml-backoffice-ui/src/settings.ts
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx
@@ -14,18 +14,20 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-export interface UiSettings {
- backendBaseURL?: string;
- signupEmail?: string;
-}
-
/**
- * Global settings for the UI.
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
*/
-const defaultSettings: UiSettings = {
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { InsertLostView } from "./views.js";
+
+export default {
+ title: "dev-experiment",
};
-export const uiSettings: UiSettings =
- "talerExchangeAmlSettings" in globalThis
- ? (globalThis as any).talerExchangeAmlSettings
- : defaultSettings;
+export const Ready = tests.createExample(InsertLostView, {
+ status: "insertLost",
+ confirm: {},
+ error: undefined,
+});
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts
new file mode 100644
index 000000000..d4f2ca8b1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts
@@ -0,0 +1,65 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { createWalletApiMock } from "../../test-utils.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+
+describe("DevExperiment CTA states", () => {
+ it("should tell the user that the URI is missing", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ const props: Props = {
+ talerExperimentUri: undefined,
+ onCancel: async () => {
+ null;
+ },
+ onSuccess: async () => {
+ null;
+ },
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("error");
+ },
+ ({ status, error }) => {
+ expect(status).equals("error");
+
+ if (!error) expect.fail();
+ // if (!error.hasError) expect.fail();
+ // if (error.operational) expect.fail();
+ // expect(error.description).eq("ERROR_NO-URI-FOR-DEPOSIT");
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+});
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx b/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx
new file mode 100644
index 000000000..afad17ad1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx
@@ -0,0 +1,74 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Amount } from "../../components/Amount.js";
+import { Part } from "../../components/Part.js";
+import { Button } from "../../mui/Button.js";
+import { State } from "./index.js";
+
+/**
+ *
+ * @author sebasjm
+ */
+
+export function InsertLostView(state: State.InsertLost): VNode {
+ const { i18n } = useTranslationContext();
+ return <Fragment>
+ <section>
+ <Part
+ title={i18n.str`Experiment`}
+ text={i18n.str`Insert lost denomination`}
+ />
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.confirm.onClick}
+ >
+ <i18n.Translate>Apply</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+}
+
+export function InsertPendingRefreshView(state: State.PendingRefresh): VNode {
+ const { i18n } = useTranslationContext();
+ return <Fragment>
+ <section>
+ <Part
+ title={i18n.str`Experiment`}
+ text={i18n.str`Pending refresh`}
+ />
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.confirm.onClick}
+ >
+ <i18n.Translate>Apply</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+}
+
+export function UnknownView(state: State.Unknown): VNode {
+ return <div>unknown experiment "{state.experimentId}"</div>
+}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
index fa7127fc0..e2c37fbba 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
@@ -18,13 +18,9 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, h, VNode } from "preact";
import { Part } from "../../components/Part.js";
-import {
- SvgIcon
-} from "../../components/styled/index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
import { Button } from "../../mui/Button.js";
import { TextField } from "../../mui/TextField.js";
-import editIcon from "../../svg/edit_24px.inline.svg";
import {
ExchangeDetails,
getAmountWithFee,
@@ -39,7 +35,7 @@ export function ReadyView({
create,
toBeReceived,
requestAmount,
- doSelectExchange,
+ doSelectExchange: _doSelectExchange,
}: State.Ready): VNode {
const { i18n } = useTranslationContext();
@@ -58,10 +54,10 @@ export function ReadyView({
);
}
}
- async function _20DaysExpiration(): Promise<void> {
+ async function _30DaysExpiration(): Promise<void> {
if (expiration.onInput) {
expiration.onInput(
- format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"),
+ format(new Date().getTime() + 1000 * 60 * 60 * 24 * 30, "dd/MM/yyyy"),
);
}
}
@@ -131,9 +127,9 @@ export function ReadyView({
<Button
variant="outlined"
disabled={!expiration.onInput}
- onClick={_20DaysExpiration}
+ onClick={_30DaysExpiration}
>
- 20 days
+ 30 days
</Button>
</p>
</p>
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
index ef135c1ba..547d5ac9a 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
@@ -16,7 +16,6 @@
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
-import { Amount } from "../../components/Amount.js";
import { Part } from "../../components/Part.js";
import { PaymentButtons } from "../../components/PaymentButtons.js";
import { Time } from "../../components/Time.js";
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
index 1542c8f29..8bbb8dac2 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -21,18 +21,16 @@ import {
PreparePayResultType,
TranslatedString,
} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { Part } from "../../components/Part.js";
import { PaymentButtons } from "../../components/PaymentButtons.js";
-import { SuccessBox, WarningBox } from "../../components/styled/index.js";
+import { ShowFullContractTermPopup } from "../../components/ShowFullContractTermPopup.js";
import { Time } from "../../components/Time.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import {
- getAmountWithFee,
- MerchantDetails,
- PurchaseDetails,
-} from "../../wallet/Transaction.js";
+import { SuccessBox, WarningBox } from "../../components/styled/index.js";
+import { MerchantDetails } from "../../wallet/Transaction.js";
import { State } from "./index.js";
+import { EnabledBySettings } from "../../components/EnabledBySettings.js";
type SupportedStates =
| State.Ready
@@ -67,29 +65,6 @@ export function BaseView(state: SupportedStates): VNode {
text={<MerchantDetails merchant={contractTerms.merchant} />}
kind="neutral"
/>
- <Part
- title={i18n.str`Details`}
- text={
- <PurchaseDetails
- price={getAmountWithFee(effective, state.amount, "debit")}
- info={{
- ...contractTerms,
- orderId: contractTerms.order_id,
- contractTermsHash: "",
- // products: contractTerms.products!,
- }}
- proposalId={state.payStatus.transactionId}
- />
- }
- kind="neutral"
- />
- {contractTerms.order_id && (
- <Part
- title={i18n.str`Receipt`}
- text={`#${contractTerms.order_id}` as TranslatedString}
- kind="neutral"
- />
- )}
{contractTerms.pay_deadline && (
<Part
title={i18n.str`Valid until`}
@@ -105,6 +80,13 @@ export function BaseView(state: SupportedStates): VNode {
/>
)}
</section>
+ <EnabledBySettings name="advancedMode">
+ <section style={{ textAlign: "left" }}>
+ <ShowFullContractTermPopup
+ transactionId={state.payStatus.transactionId}
+ />
+ </section>
+ </EnabledBySettings>
<PaymentButtons
amount={effective}
payStatus={state.payStatus}
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
index b9257215f..6b4584fea 100644
--- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts } from "@gnu-taler/taler-util";
+import { Amounts, PreparePayResult } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
@@ -54,7 +54,7 @@ export function useComponentState({
const hook = useAsyncAsHook(async () => {
if (!talerTemplateUri) throw Error("ERROR_NO-URI-FOR-PAYMENT-TEMPLATE");
- let payStatus;
+ let payStatus: PreparePayResult | undefined = undefined;
if (!amountParam && !summaryParam) {
payStatus = await api.wallet.call(
WalletApiOperation.PreparePayForTemplate,
@@ -125,7 +125,9 @@ export function useComponentState({
},
);
setNewOrder(payStatus.talerUri!);
- } catch (e) {}
+ } catch (e) {
+ console.error(e);
+ }
}
const errors = undefinedIfEmpty({
amount: amount && Amounts.isZero(amount) ? i18n.str`required` : undefined,
@@ -164,7 +166,9 @@ export function useComponentState({
}
function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
- return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, unknown>)[k] !== undefined,
+ )
? obj
: undefined;
}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
index 8489b0643..bc855f33d 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
@@ -50,10 +50,10 @@ export function ReadyView({
);
}
}
- async function _20DaysExpiration() {
+ async function _30DaysExpiration() {
if (expiration.onInput) {
expiration.onInput(
- format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"),
+ format(new Date().getTime() + 1000 * 60 * 60 * 24 * 30, "dd/MM/yyyy"),
);
}
}
@@ -100,9 +100,9 @@ export function ReadyView({
<Button
variant="outlined"
disabled={!expiration.onInput}
- onClick={_20DaysExpiration}
+ onClick={_30DaysExpiration}
>
- 20 days
+ 30 days
</Button>
</p>
</p>
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
index 05aef690e..044f2434f 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -14,15 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/* eslint-disable react-hooks/rules-of-hooks */
import {
AmountJson,
Amounts,
ExchangeFullDetails,
ExchangeListItem,
NotificationType,
- TalerError,
- parseWithdrawExchangeUri,
+ parseWithdrawExchangeUri
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -54,7 +52,7 @@ export function useComponentStateFromParams({
: undefined;
const exchangeByTalerUri = uri?.exchangeBaseUrl;
let ex: ExchangeFullDetails | undefined;
- if (exchangeByTalerUri && uri.exchangePub) {
+ if (exchangeByTalerUri) {
await api.wallet.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchangeByTalerUri,
masterPub: uri.exchangePub,
@@ -141,8 +139,8 @@ export function useComponentStateFromParams({
confirm: {
onClick: isValid
? pushAlertOnError(async () => {
- onAmountChanged(Amounts.stringify(amount));
- })
+ onAmountChanged(Amounts.stringify(amount));
+ })
: undefined,
},
amount: {
@@ -209,7 +207,7 @@ export function useComponentStateFromURI({
WalletApiOperation.GetWithdrawalDetailsForUri,
{
talerWithdrawUri,
- notifyChangeFromPendingTimeoutMs: 30 * 1000,
+ // notifyChangeFromPendingTimeoutMs: 30 * 1000,
},
);
const {
@@ -244,9 +242,7 @@ export function useComponentStateFromURI({
}
return api.listener.onUpdateNotification(
[NotificationType.WithdrawalOperationTransition],
- (asd) => {
- uriInfoHook.retry();
- },
+ uriInfoHook.retry,
);
}, [readyToListen]);
@@ -377,9 +373,6 @@ function exchangeSelectionState(
};
}, []);
- const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
- undefined,
- );
const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
async function doWithdrawAndCheckError(): Promise<void> {
@@ -395,9 +388,9 @@ function exchangeSelectionState(
onSuccess(res.transactionId);
}
} catch (e) {
- if (e instanceof TalerError) {
- setWithdrawError(e);
- }
+ console.error(e);
+ // if (e instanceof TalerError) {
+ // }
}
setDoingWithdraw(false);
}
@@ -439,12 +432,12 @@ function exchangeSelectionState(
//TODO: calculate based on exchange info
const ageRestriction = ageRestrictionEnabled
? {
- list: ageRestrictionOptions,
- value: String(ageRestricted),
- onChange: pushAlertOnError(async (v: string) =>
- setAgeRestricted(parseInt(v, 10)),
- ),
- }
+ list: ageRestrictionOptions,
+ value: String(ageRestricted),
+ onChange: pushAlertOnError(async (v: string) =>
+ setAgeRestricted(parseInt(v, 10)),
+ ),
+ }
: undefined;
const altCurrencies = amountHook.response.accounts
@@ -454,6 +447,7 @@ function exchangeSelectionState(
altCurrencies.length === 0
? []
: [toBeReceived.currency, ...altCurrencies];
+
const convAccount = amountHook.response.accounts.find((c) => {
return (
c.currencySpecification &&
@@ -463,9 +457,9 @@ function exchangeSelectionState(
const conversionInfo = !convAccount
? undefined
: {
- spec: convAccount.currencySpecification!,
- amount: Amounts.parseOrThrow(convAccount.transferAmount!),
- };
+ spec: convAccount.currencySpecification!,
+ amount: Amounts.parseOrThrow(convAccount.transferAmount!),
+ };
return {
status: "success",
diff --git a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
index a5e357f7d..bd430f2ef 100644
--- a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
@@ -48,14 +48,13 @@ export type HookResponseWithRetry<T> =
export function useAsyncAsHook<T>(
fn: () => Promise<T | false>,
- deps?: any[],
+ deps?: unknown[],
): HookResponseWithRetry<T> {
const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
const args = useMemo(
() => ({
fn,
- // eslint-disable-next-line react-hooks/exhaustive-deps
}),
deps || [],
);
diff --git a/packages/taler-wallet-webextension/src/i18n/de.po b/packages/taler-wallet-webextension/src/i18n/de.po
index 1a285499c..bc66f2136 100644
--- a/packages/taler-wallet-webextension/src/i18n/de.po
+++ b/packages/taler-wallet-webextension/src/i18n/de.po
@@ -17,7 +17,7 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2023-11-25 17:24+0000\n"
+"PO-Revision-Date: 2024-05-07 14:32+0000\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/de/>\n"
@@ -26,7 +26,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 5.2.1\n"
+"X-Generator: Weblate 5.4.3\n"
#: src/NavigationBar.tsx:139
#, c-format
@@ -56,7 +56,7 @@ msgstr "Dev"
#: src/mui/Typography.tsx:122
#, c-format
msgid "%1$s"
-msgstr ""
+msgstr "%1$s"
#: src/components/PendingTransactions.tsx:74
#, c-format
@@ -215,7 +215,7 @@ msgstr ""
#: src/wallet/AddNewActionView.tsx:57
#, c-format
msgid "Cancel"
-msgstr "Abbrechen"
+msgstr "Zurück"
#: src/wallet/AddNewActionView.tsx:68
#, c-format
@@ -325,7 +325,7 @@ msgstr ""
#: src/components/ShowFullContractTermPopup.tsx:189
#, c-format
msgid "Summary"
-msgstr ""
+msgstr "Zusammenfassung"
#: src/components/ShowFullContractTermPopup.tsx:195
#, c-format
@@ -370,7 +370,7 @@ msgstr ""
#: src/components/ShowFullContractTermPopup.tsx:256
#, c-format
msgid "Delivery date"
-msgstr ""
+msgstr "Lieferdatum"
#: src/components/ShowFullContractTermPopup.tsx:271
#, c-format
@@ -405,7 +405,7 @@ msgstr ""
#: src/components/ShowFullContractTermPopup.tsx:354
#, c-format
msgid "Fulfillment URL"
-msgstr ""
+msgstr "Adresse digitaler Dienstleistung (Fulfillment-URL)"
#: src/components/ShowFullContractTermPopup.tsx:360
#, c-format
@@ -1061,7 +1061,7 @@ msgstr "Konnte die Umsatzanzeige nicht laden"
#: src/wallet/ExchangeSelection/views.tsx:131
#, c-format
msgid "Close"
-msgstr ""
+msgstr "Schließen"
#: src/wallet/ExchangeSelection/views.tsx:160
#, fuzzy, c-format
diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx
index 1af281d42..12a4d91ea 100644
--- a/packages/taler-wallet-webextension/src/mui/Button.tsx
+++ b/packages/taler-wallet-webextension/src/mui/Button.tsx
@@ -371,7 +371,11 @@ function ButtonBase({
);
}
return (
- <button onClick={doClick} class={classNames} {...rest}>
+ <button onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ doClick();
+ }} class={classNames} {...rest}>
{children}
</button>
);
diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts
index e92903981..3c116fab2 100644
--- a/packages/taler-wallet-webextension/src/platform/api.ts
+++ b/packages/taler-wallet-webextension/src/platform/api.ts
@@ -18,11 +18,9 @@ import {
CoreApiResponse,
TalerUri,
WalletNotification,
- WalletRunConfig
+ WalletRunConfig,
} from "@gnu-taler/taler-util";
-import {
- WalletOperations
-} from "@gnu-taler/taler-wallet-core";
+import { WalletOperations } from "@gnu-taler/taler-wallet-core";
import {
ExtensionOperations,
MessageFromExtension,
@@ -46,11 +44,9 @@ export interface Permissions {
* Compatibility API that works on multiple browsers.
*/
export interface CrossBrowserPermissionsApi {
-
containsClipboardPermissions(): Promise<boolean>;
requestClipboardPermissions(): Promise<boolean>;
removeClipboardPermissions(): Promise<boolean>;
-
}
export enum ExtensionNotificationType {
@@ -67,25 +63,29 @@ export interface ClearNotificaitonNotification {
type: ExtensionNotificationType.ClearNotifications;
}
-export type ExtensionNotification = SettingsChangeNotification | ClearNotificaitonNotification
+export type ExtensionNotification =
+ | SettingsChangeNotification
+ | ClearNotificaitonNotification;
-export type MessageFromBackend = {
- type: "wallet",
- notification: WalletNotification
-} | {
- type: "web-extension",
- notification: ExtensionNotification
-};
+export type MessageFromBackend =
+ | {
+ type: "wallet";
+ notification: WalletNotification;
+ }
+ | {
+ type: "web-extension";
+ notification: ExtensionNotification;
+ };
export type MessageFromFrontend<
Op extends BackgroundOperations | WalletOperations | ExtensionOperations,
> = Op extends BackgroundOperations
? MessageFromFrontendBackground<keyof BackgroundOperations>
: Op extends ExtensionOperations
- ? MessageFromExtension<keyof ExtensionOperations>
- : Op extends WalletOperations
- ? MessageFromFrontendWallet<keyof WalletOperations>
- : never;
+ ? MessageFromExtension<keyof ExtensionOperations>
+ : Op extends WalletOperations
+ ? MessageFromFrontendWallet<keyof WalletOperations>
+ : never;
export type MessageFromFrontendBackground<
Op extends keyof BackgroundOperations,
@@ -109,7 +109,6 @@ export interface WalletWebExVersion {
}
type F = WalletRunConfig["features"];
-type kf = keyof F;
type WebexWalletConfig = {
[P in keyof F as `wallet${Capitalize<P>}`]: F[P];
};
@@ -231,7 +230,13 @@ export interface BackgroundPlatformAPI {
) => Promise<MessageResponse>,
): void;
+ /**
+ * Change web extension Icon
+ */
+ setAlertedIcon(): void;
+ setNormalIcon(): void;
}
+
export interface ForegroundPlatformAPI {
/**
* Check if the extension is running under
@@ -270,7 +275,7 @@ export interface ForegroundPlatformAPI {
/**
* Open a page and close the popup
- * @param url
+ * @param url
*/
openNewURLFromPopup(url: URL): void;
/**
@@ -309,9 +314,9 @@ export interface ForegroundPlatformAPI {
): Promise<MessageResponse>;
/**
- * Used by the wallet frontend to send notification about new information
- * @param message
- */
+ * Used by the wallet frontend to send notification about new information
+ * @param message
+ */
triggerWalletEvent(message: MessageFromBackend): void;
/**
diff --git a/packages/taler-wallet-webextension/src/platform/background.ts b/packages/taler-wallet-webextension/src/platform/background.ts
index 9f3764c25..13808af2b 100644
--- a/packages/taler-wallet-webextension/src/platform/background.ts
+++ b/packages/taler-wallet-webextension/src/platform/background.ts
@@ -16,7 +16,8 @@
import { BackgroundPlatformAPI } from "./api.js";
-export let platform: BackgroundPlatformAPI = undefined as any;
+// it should never be undefined :)
+export let platform: BackgroundPlatformAPI = undefined!;
export function setupPlatform(impl: BackgroundPlatformAPI): void {
platform = impl;
}
diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts
index ee071347a..e63040f5c 100644
--- a/packages/taler-wallet-webextension/src/platform/chrome.ts
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -53,7 +53,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
redirectTabToWalletPage,
registerAllIncomingConnections,
registerOnInstalled,
- listenToAllChannels: listenToAllChannels as any,
+ listenToAllChannels ,
registerReloadOnNewVersion,
sendMessageToAllChannels,
openNewURLFromPopup,
@@ -61,6 +61,8 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
useServiceWorkerAsBackgroundProcess,
keepAlive,
listenNetworkConnectionState,
+ setAlertedIcon,
+ setNormalIcon,
};
export default api;
@@ -69,7 +71,7 @@ const logger = new Logger("chrome.ts");
const WALLET_STORAGE_KEY = "wallet-settings";
-function jsonParseOrDefault(unparsed: any, def: any) {
+function jsonParseOrDefault(unparsed: string, def: unknown) {
if (!unparsed) return def;
try {
return JSON.parse(unparsed);
@@ -85,7 +87,7 @@ async function getSettingsFromStorage(): Promise<Settings> {
return jsonParseOrDefault(settings, defaultSettings);
}
-function keepAlive(callback: any): void {
+function keepAlive(callback: () => void): void {
if (extensionIsManifestV3()) {
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
@@ -103,7 +105,7 @@ function isFirefox(): boolean {
}
export function containsClipboardPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
+ return new Promise((res) => {
res(false);
// chrome.permissions.contains({ permissions: ["clipboardRead"] }, (resp) => {
// const le = chrome.runtime.lastError?.message;
@@ -116,7 +118,7 @@ export function containsClipboardPermissions(): Promise<boolean> {
}
export async function requestClipboardPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
+ return new Promise((res) => {
res(false);
// chrome.permissions.request({ permissions: ["clipboardRead"] }, (resp) => {
// const le = chrome.runtime.lastError?.message;
@@ -129,7 +131,7 @@ export async function requestClipboardPermissions(): Promise<boolean> {
}
export function removeClipboardPermissions(): Promise<boolean> {
- return new Promise((res, rej) => {
+ return new Promise((res) => {
res(true);
// chrome.permissions.remove({ permissions: ["clipboardRead"] }, (resp) => {
// const le = chrome.runtime.lastError?.message;
@@ -154,7 +156,7 @@ function getPermissionsApi(): CrossBrowserPermissionsApi {
* @param callback function to be called
*/
function notifyWhenAppIsReady(): Promise<void> {
- return new Promise((resolve, reject) => {
+ return new Promise((resolve) => {
if (extensionIsManifestV3()) {
resolve();
} else {
@@ -221,6 +223,13 @@ function openWalletURIFromPopup(uri: TalerUri): void {
)}`,
);
break;
+ case TalerUriAction.AddExchange:
+ url = chrome.runtime.getURL(
+ `static/wallet.html#/cta/add/exchange?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
+ );
+ break;
case TalerUriAction.DevExperiment:
logger.warn(`taler://dev-experiment URIs are not allowed in headers`);
return;
@@ -269,7 +278,7 @@ async function sendMessageToBackground<
nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);
const messageWithId = { ...message, id: `id_${nextMessageIndex}` };
- return new Promise<any>((resolve, reject) => {
+ return new Promise<MessageResponse>((resolve, reject) => {
logger.trace("send operation to the wallet background", message);
let timedout = false;
const timerId = setTimeout(() => {
@@ -300,7 +309,7 @@ async function sendMessageToBackground<
* To be used by the foreground
*/
let notificationPort: chrome.runtime.Port | undefined;
-function listenToWalletBackground(listener: (m: any) => void): () => void {
+function listenToWalletBackground(listener: (message: MessageFromBackend) => void): () => void {
if (notificationPort === undefined) {
notificationPort = chrome.runtime.connect({ name: "notifications" });
}
@@ -362,7 +371,7 @@ function registerAllIncomingConnections(): void {
notification: {
type: ExtensionNotificationType.SettingsChange,
currentValue: jsonParseOrDefault(
- event[WALLET_STORAGE_KEY],
+ event[WALLET_STORAGE_KEY].newValue,
defaultSettings,
),
},
@@ -408,12 +417,12 @@ function registerReloadOnNewVersion(): void {
});
}
-async function redirectCurrentTabToWalletPage(page: string): Promise<void> {
- let queryOptions = { active: true, lastFocusedWindow: true };
- let [tab] = await chrome.tabs.query(queryOptions);
+// async function redirectCurrentTabToWalletPage(page: string): Promise<void> {
+// let queryOptions = { active: true, lastFocusedWindow: true };
+// let [tab] = await chrome.tabs.query(queryOptions);
- return redirectTabToWalletPage(tab.id!, page);
-}
+// return redirectTabToWalletPage(tab.id!, page);
+// }
async function redirectTabToWalletPage(
tabId: number,
@@ -474,26 +483,26 @@ function setAlertedIcon(): void {
interface OffscreenCanvasRenderingContext2D
extends CanvasState,
- CanvasTransform,
- CanvasCompositing,
- CanvasImageSmoothing,
- CanvasFillStrokeStyles,
- CanvasShadowStyles,
- CanvasFilters,
- CanvasRect,
- CanvasDrawPath,
- CanvasUserInterface,
- CanvasText,
- CanvasDrawImage,
- CanvasImageData,
- CanvasPathDrawingStyles,
- CanvasTextDrawingStyles,
- CanvasPath {
+ CanvasTransform,
+ CanvasCompositing,
+ CanvasImageSmoothing,
+ CanvasFillStrokeStyles,
+ CanvasShadowStyles,
+ CanvasFilters,
+ CanvasRect,
+ CanvasDrawPath,
+ CanvasUserInterface,
+ CanvasText,
+ CanvasDrawImage,
+ CanvasImageData,
+ CanvasPathDrawingStyles,
+ CanvasTextDrawingStyles,
+ CanvasPath {
readonly canvas: OffscreenCanvas;
}
declare const OffscreenCanvasRenderingContext2D: {
prototype: OffscreenCanvasRenderingContext2D;
- new (): OffscreenCanvasRenderingContext2D;
+ new(): OffscreenCanvasRenderingContext2D;
};
interface OffscreenCanvas extends EventTarget {
@@ -506,7 +515,7 @@ interface OffscreenCanvas extends EventTarget {
}
declare const OffscreenCanvas: {
prototype: OffscreenCanvas;
- new (width: number, height: number): OffscreenCanvas;
+ new(width: number, height: number): OffscreenCanvas;
};
function createCanvas(size: number): OffscreenCanvas {
@@ -659,7 +668,7 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
return;
}
} else {
- return new Promise((resolve, reject) => {
+ return new Promise((resolve) => {
//manifest v2
chrome.tabs.executeScript(
tabId,
@@ -685,9 +694,9 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
}
}
-async function timeout(ms: number): Promise<void> {
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
+// async function timeout(ms: number): Promise<void> {
+// return new Promise((resolve) => setTimeout(resolve, ms));
+// }
async function findTalerUriInClipboard(): Promise<string | undefined> {
//FIXME: add clipboard feature
// try {
diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts
index 1e43476ea..d6e743147 100644
--- a/packages/taler-wallet-webextension/src/platform/dev.ts
+++ b/packages/taler-wallet-webextension/src/platform/dev.ts
@@ -38,6 +38,8 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
listenNetworkConnectionState,
openNewURLFromPopup: () => undefined,
triggerWalletEvent: () => undefined,
+ setAlertedIcon: () => undefined,
+ setNormalIcon : () => undefined,
getPermissionsApi: () => ({
containsClipboardPermissions: async () => true,
removeClipboardPermissions: async () => false,
diff --git a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
index 2fc21bb56..c698066e7 100644
--- a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
+++ b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
@@ -45,7 +45,7 @@ export function NoBalanceHelp({
</Button>
</Alert>
</Paper>
- <a href="https://demo.taler.net/" style={{ display: "block" }}>
+ <a target="_bank" rel="noreferrer" href="https://demo.taler.net/" style={{ display: "block" }}>
<i18n.Translate>Try the demo bank and withdraw test money.</i18n.Translate> »
</a>
</Fragment>
diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
index 11a888412..21373c7cd 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
@@ -77,6 +77,17 @@ function ContentByUriType({
</Button>
</div>
);
+ case TalerUriAction.AddExchange:
+ return (
+ <div>
+ <p>
+ <i18n.Translate>This page has a add exchange action.</i18n.Translate>
+ </p>
+ <Button variant="contained" color="success" onClick={onConfirm}>
+ <i18n.Translate>Open add exchange page</i18n.Translate>
+ </Button>
+ </div>
+ );
case TalerUriAction.DevExperiment:
case TalerUriAction.PayPull:
diff --git a/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg
new file mode 100644
index 000000000..d880cbf0f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" height="24" width="24">
+ <path fill-rule="evenodd" d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z" clip-rule="evenodd" />
+</svg>
+
diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts
index 90037819f..452cc578e 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -190,7 +190,7 @@ export function createWalletApiMock(): {
bankCore: new TalerCoreBankHttpClient("/"),
bankIntegration: new TalerBankIntegrationHttpClient("/"),
bankWire: new TalerWireGatewayHttpClient("/",""),
- bankRevenue: new TalerRevenueHttpClient("/",""),
+ bankRevenue: new TalerRevenueHttpClient("/"),
}
children = create(ApiContextProvider, { value, children }, children);
children = create(
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
index d59501212..94b32c157 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
@@ -14,14 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { OperationFailWithBody, OperationOk, OperationResult, TalerExchangeApi } from "@gnu-taler/taler-util";
+import { OperationFailWithBody, OperationOk, TalerExchangeApi } from "@gnu-taler/taler-util";
import { ErrorAlertView } from "../../components/CurrentAlerts.js";
import { Loading } from "../../components/Loading.js";
import { ErrorAlert } from "../../context/alert.js";
import { TextFieldHandler } from "../../mui/handlers.js";
-import { compose, StateViewMap } from "../../utils/index.js";
+import { StateViewMap, compose } from "../../utils/index.js";
import { useComponentState } from "./state.js";
-import { ConfirmView, VerifyView } from "./views.js";
+import { ConfirmAddExchangeView, VerifyView } from "./views.js";
export interface Props {
currency?: string;
@@ -37,6 +37,7 @@ export type State = State.Loading
export type CheckExchangeErrors = {
"invalid-version": string;
"invalid-currency": string;
+ "not-found": void;
"already-active": void;
"invalid-protocol": void;
}
@@ -80,7 +81,7 @@ export namespace State {
const viewMapping: StateViewMap<State> = {
loading: Loading,
error: ErrorAlertView,
- confirm: ConfirmView,
+ confirm: ConfirmAddExchangeView,
verify: VerifyView,
};
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
index 5ae0aa8f4..4a04f762a 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
@@ -14,15 +14,15 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ExchangeEntryStatus, OperationFailWithBody, OperationOk, TalerExchangeApi, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailureWithBody } from "@gnu-taler/taler-util";
+import { ExchangeEntryStatus, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailureWithBody } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
import { useCallback, useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { withSafe } from "../../mui/handlers.js";
import { RecursiveState } from "../../utils/index.js";
import { CheckExchangeErrors, Props, State } from "./index.js";
-import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
function urlFromInput(str: string): URL {
let result: URL;
@@ -83,6 +83,9 @@ export function useComponentState({ onBack, currency, noDebounce }: Props): Recu
*/
const api = new TalerExchangeHttpClient(baseUrl.href, new BrowserFetchHttpLib() as any);
const config = await api.getConfig()
+ if (config.type === "fail") {
+ return opKnownFailureWithBody<CheckExchangeErrors>("not-found", undefined)
+ }
if (!api.isCompatible(config.body.version)) {
return opKnownFailureWithBody<CheckExchangeErrors>("invalid-version", config.body.version)
}
@@ -155,7 +158,7 @@ function useDebounce<T>(
const [result, setResult] = useState<T | undefined>(undefined);
const [error, setError] = useState<Error | undefined>(undefined);
- const [handler, setHandler] = useState<any | undefined>(undefined);
+ const [handler, setHandler] = useState<number | undefined>(undefined);
if (!disabled) {
useEffect(() => {
@@ -180,7 +183,7 @@ function useDebounce<T>(
setResult(undefined);
}
}, 500);
- setHandler(h);
+ setHandler(h as unknown as number);
}, [value, setHandler, onTrigger]);
}
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx b/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx
index 4e2610743..f205b6415 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx
@@ -19,8 +19,6 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import * as tests from "@gnu-taler/web-util/testing";
-import { ConfirmView, VerifyView } from "./views.js";
export default {
title: "example",
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
index aa844f175..d0e78a94e 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
@@ -23,6 +23,7 @@ import {
ExchangeEntryStatus,
ExchangeTosStatus,
ExchangeUpdateStatus,
+ ScopeType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import * as tests from "@gnu-taler/web-util/testing";
@@ -48,7 +49,11 @@ describe("AddExchange states", () => {
{
exchangeBaseUrl: "http://exchange.local/",
ageRestrictionOptions: [],
- scopeInfo: undefined,
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://exchange.local/",
+ },
masterPub: "123qwe123",
currency: "ARS",
exchangeEntryStatus: ExchangeEntryStatus.Ephemeral,
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
index 489d7eb3b..f6537bc68 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
@@ -17,13 +17,18 @@
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { ErrorMessage } from "../../components/ErrorMessage.js";
-import { Input, LightText, SubTitle, Title, WarningBox } from "../../components/styled/index.js";
+import {
+ Input,
+ LightText,
+ SubTitle,
+ Title,
+ WarningBox,
+} from "../../components/styled/index.js";
import { TermsOfService } from "../../components/TermsOfService/index.js";
import { Button } from "../../mui/Button.js";
import { State } from "./index.js";
import { assertUnreachable } from "@gnu-taler/taler-util";
-
export function VerifyView({
expectedCurrency,
onCancel,
@@ -57,40 +62,71 @@ export function VerifyView({
{(() => {
if (!result) return;
if (result.type == "ok") {
- return <LightText>
- <i18n.Translate>
- An exchange has been found! Review the information and click next
- </i18n.Translate>
- </LightText>
+ return (
+ <LightText>
+ <i18n.Translate>
+ An exchange has been found! Review the information and click
+ next
+ </i18n.Translate>
+ </LightText>
+ );
}
switch (result.case) {
case "already-active": {
- return <WarningBox>
- <i18n.Translate>This exchange is already in your list.</i18n.Translate>
- </WarningBox>
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange is already in your list.
+ </i18n.Translate>
+ </WarningBox>
+ );
}
case "invalid-protocol": {
- return <WarningBox>
- <i18n.Translate>Only exchange accessible through "http" and "https" are allowed.</i18n.Translate>
- </WarningBox>
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ Only exchange accessible through "http" and "https" are
+ allowed.
+ </i18n.Translate>
+ </WarningBox>
+ );
}
case "invalid-version": {
- return <WarningBox>
- <i18n.Translate>This exchange protocol version is not supported: "{result.body}".</i18n.Translate>
- </WarningBox>
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange protocol version is not supported: "
+ {result.body}".
+ </i18n.Translate>
+ </WarningBox>
+ );
}
case "invalid-currency": {
- return <WarningBox>
- <i18n.Translate>This exchange currency "{result.body}" doesn&apos;t match the expected currency {expectedCurrency}.</i18n.Translate>
- </WarningBox>
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange currency "{result.body}" doesn&apos;t match
+ the expected currency {expectedCurrency}.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "not-found": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ No exchange found in that URL.
+ </i18n.Translate>
+ </WarningBox>
+ );
}
default: {
- assertUnreachable(result.case)
+ assertUnreachable(result.case);
}
}
})()}
<p>
- <Input invalid={result && result.type !== "ok"} >
+ <Input invalid={result && result.type !== "ok"}>
<label>URL</label>
<input
type="text"
@@ -98,7 +134,7 @@ export function VerifyView({
value={url.value}
onInput={(e) => {
if (url.onInput) {
- url.onInput(e.currentTarget.value)
+ url.onInput(e.currentTarget.value);
}
}}
/>
@@ -138,10 +174,7 @@ export function VerifyView({
</Button>
<Button
variant="contained"
- disabled={
- !result ||
- result.type !== "ok"
- }
+ disabled={!result || result.type !== "ok"}
onClick={onAccept}
>
<i18n.Translate>Next</i18n.Translate>
@@ -149,14 +182,22 @@ export function VerifyView({
</footer>
<section>
<ul>
- {knownExchanges.map(ex => {
- return <li><a href="#" onClick={(e) => {
- if (url.onInput) {
- url.onInput(ex.href)
- }
- e.preventDefault()
- }}>
- {ex.href}</a></li>
+ {knownExchanges.map((ex) => {
+ return (
+ <li key={ex.href}>
+ <a
+ href="#"
+ onClick={(e) => {
+ if (url.onInput) {
+ url.onInput(ex.href);
+ }
+ e.preventDefault();
+ }}
+ >
+ {ex.href}
+ </a>
+ </li>
+ );
})}
</ul>
</section>
@@ -164,8 +205,7 @@ export function VerifyView({
);
}
-
-export function ConfirmView({
+export function ConfirmAddExchangeView({
url,
onCancel,
onConfirm,
@@ -186,8 +226,7 @@ export function ConfirmView({
</div>
</section>
-
- <TermsOfService key="terms" exchangeUrl={url} >
+ <TermsOfService key="terms" exchangeUrl={url}>
<footer>
<Button
key="cancel"
diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx
index 62a519f06..884c2eab7 100644
--- a/packages/taler-wallet-webextension/src/wallet/Application.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx
@@ -23,7 +23,9 @@
import {
Amounts,
TalerUri,
+ TalerUriAction,
TranslatedString,
+ parseTalerUri,
stringifyTalerUri,
} from "@gnu-taler/taler-util";
import {
@@ -83,6 +85,8 @@ import { TransactionPage } from "./Transaction.js";
import { WelcomePage } from "./Welcome.js";
import { WalletActivity } from "../components/WalletActivity.js";
import { EnabledBySettings } from "../components/EnabledBySettings.js";
+import { DevExperimentPage } from "../cta/DevExperiment/index.js";
+import { ConfirmAddExchangeView } from "./AddExchange/views.js";
export function Application(): VNode {
const { i18n } = useTranslationContext();
@@ -153,7 +157,7 @@ export function Application(): VNode {
)}
/>
- <Route
+<Route
path={Pages.balanceHistory.pattern}
component={({ currency }: { currency?: string }) => (
<WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
@@ -174,6 +178,27 @@ export function Application(): VNode {
)}
/>
<Route
+ path={Pages.searchHistory.pattern}
+ component={({ currency }: { currency?: string }) => (
+ <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <HistoryPage
+ currency={currency}
+ search
+ goToWalletDeposit={(currency: string) =>
+ redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ }
+ goToWalletManualWithdraw={(currency?: string) =>
+ redirectTo(
+ Pages.receiveCash({
+ amount: !currency ? undefined : `${currency}:0`,
+ }),
+ )
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
path={Pages.sendCash.pattern}
component={({ amount }: { amount?: string }) => (
<WalletTemplate path="balance" goToURL={redirectToURL}>
@@ -497,7 +522,40 @@ export function Application(): VNode {
</CallToActionTemplate>
)}
/>
-
+ <Route
+ path={Pages.ctaExperiment}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Development experiment`}>
+ <DevExperimentPage
+ talerExperimentUri={decodeURIComponent(talerUri)}
+ onCancel={() => redirectTo(Pages.balanceHistory({}))}
+ onSuccess={() => redirectTo(Pages.balanceHistory({}))}
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaAddExchange}
+ component={({ talerUri }: { talerUri: string }) => {
+ const tUri = parseTalerUri(decodeURIComponent(talerUri))
+ const baseUrl = tUri?.type === TalerUriAction.AddExchange ? tUri.exchangeBaseUrl : undefined
+ if (!baseUrl) {
+ redirectTo(Pages.balanceHistory({}))
+ return <div>
+ invalid url {talerUri}
+ </div>
+ }
+ return <CallToActionTemplate title={i18n.str`Add exchange`}>
+ <ConfirmAddExchangeView
+ url={baseUrl}
+ status="confirm"
+ error={undefined}
+ onCancel={() => redirectTo(Pages.balanceHistory({}))}
+ onConfirm={() => redirectTo(Pages.balanceHistory({}))}
+ />
+ </CallToActionTemplate>
+ }}
+ />
{/**
* NOT FOUND
* all redirects should be at the end
@@ -531,17 +589,17 @@ function Redirect({ to }: { to: string }): null {
return null;
}
-function matchesRoute(url: string, route: string): boolean {
- type MatcherFunc = (
- url: string,
- route: string,
- opts: any,
- ) => Record<string, string> | false;
+// function matchesRoute(url: string, route: string): boolean {
+// type MatcherFunc = (
+// url: string,
+// route: string,
+// opts: any,
+// ) => Record<string, string> | false;
- const internalPreactMatcher: MatcherFunc = (Router as any).exec;
- const result = internalPreactMatcher(url, route, {});
- return !result ? false : true;
-}
+// const internalPreactMatcher: MatcherFunc = (Router as any).exec;
+// const result = internalPreactMatcher(url, route, {});
+// return !result ? false : true;
+// }
function CallToActionTemplate({
title,
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
index 29cf23739..683378613 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
@@ -25,6 +25,7 @@ import {
ExchangeListItem,
ExchangeTosStatus,
ExchangeUpdateStatus,
+ ScopeType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import * as tests from "@gnu-taler/web-util/testing";
@@ -37,7 +38,11 @@ const exchangeArs: ExchangeListItem = {
currency: "ARS",
exchangeBaseUrl: "http://",
masterPub: "123qwe123",
- scopeInfo: undefined,
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://",
+ },
tosStatus: ExchangeTosStatus.Accepted,
exchangeEntryStatus: ExchangeEntryStatus.Used,
exchangeUpdateStatus: ExchangeUpdateStatus.Initial,
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
index c40a3a64c..8a74a20f1 100644
--- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
@@ -169,7 +169,9 @@ const CircleDiv = styled.div`
text-align: center;
text-decoration: none;
text-transform: uppercase;
- transition: background-color 0.15s ease, border-color 0.15s ease,
+ transition:
+ background-color 0.15s ease,
+ border-color 0.15s ease,
color 0.15s ease;
font-size: 16px;
background-color: #86a7bd1a;
@@ -275,6 +277,16 @@ export function ReadyGetView({
</Button>
</Paper>
</Grid>
+ <Grid item xs={1}>
+ <Paper style={{ padding: 8 }}>
+ <p>
+ <i18n.Translate>From a <pre style={{display:"inline"}}>taler://peer-push-credit</pre> URI</i18n.Translate>
+ </p>
+ <a href={Pages.qr}>
+ <i18n.Translate>Enter URI here</i18n.Translate>
+ </a>
+ </Paper>
+ </Grid>
</Grid>
</Grid>
</Container>
diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
index c28e4188f..482b8d698 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
@@ -49,17 +49,17 @@ export default {
let count = 0;
const commonTransaction = (): TransactionCommon =>
-({
- amountRaw: "USD:10",
- amountEffective: "USD:9",
- txState: {
- major: TransactionMajorState.Done,
- },
- timestamp: TalerProtocolTimestamp.fromSeconds(
- new Date().getTime() / 1000 - count++ * 60 * 60 * 7,
- ),
- transactionId: String(count),
-} as TransactionCommon);
+ ({
+ amountRaw: "USD:10",
+ amountEffective: "USD:9",
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ timestamp: TalerProtocolTimestamp.fromSeconds(
+ new Date().getTime() / 1000 - count++ * 60 * 60 * 7,
+ ),
+ transactionId: String(count),
+ }) as TransactionCommon;
const exampleData = {
withdraw: {
@@ -165,7 +165,9 @@ const exampleData = {
export const SomeBalanceWithNoTransactions = tests.createExample(
TestedComponent,
{
- transactions: [],
+ transactionsByDate: {
+ "11/11/11": [],
+ },
balances: [
{
available: "TESTKUDOS:10" as AmountString,
@@ -186,7 +188,9 @@ export const SomeBalanceWithNoTransactions = tests.createExample(
);
export const OneSimpleTransaction = tests.createExample(TestedComponent, {
- transactions: [exampleData.withdraw],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
flags: [],
@@ -203,13 +207,14 @@ export const OneSimpleTransaction = tests.createExample(TestedComponent, {
},
],
balanceIndex: 0,
-
});
export const TwoTransactionsAndZeroBalance = tests.createExample(
TestedComponent,
{
- transactions: [exampleData.withdraw, exampleData.deposit],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw, exampleData.deposit],
+ },
balances: [
{
flags: [],
@@ -230,14 +235,16 @@ export const TwoTransactionsAndZeroBalance = tests.createExample(
);
export const OneTransactionPending = tests.createExample(TestedComponent, {
- transactions: [
- {
- ...exampleData.withdraw,
- txState: {
- major: TransactionMajorState.Pending,
+ transactionsByDate: {
+ "11/11/11": [
+ {
+ ...exampleData.withdraw,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
- },
- ],
+ ],
+ },
balances: [
{
flags: [],
@@ -257,22 +264,24 @@ export const OneTransactionPending = tests.createExample(TestedComponent, {
});
export const SomeTransactions = tests.createExample(TestedComponent, {
- transactions: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary:
- "this is a long summary that may be cropped because its too long",
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.withdraw,
+ exampleData.payment,
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary:
+ "this is a long summary that may be cropped because its too long",
+ },
},
- },
- exampleData.refund,
- exampleData.deposit,
- ],
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
flags: [],
@@ -294,79 +303,81 @@ export const SomeTransactions = tests.createExample(TestedComponent, {
export const SomeTransactionsInDifferentStates = tests.createExample(
TestedComponent,
{
- transactions: [
- exampleData.withdraw,
- {
- ...exampleData.withdraw,
- exchangeBaseUrl: "https://aborted/withdrawal",
- txState: {
- major: TransactionMajorState.Aborted,
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://aborted/withdrawal",
+ txState: {
+ major: TransactionMajorState.Aborted,
+ },
},
- },
- {
- ...exampleData.withdraw,
- exchangeBaseUrl: "https://pending/withdrawal",
- txState: {
- major: TransactionMajorState.Pending,
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://pending/withdrawal",
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
- },
- {
- ...exampleData.withdraw,
- exchangeBaseUrl: "https://failed/withdrawal",
- txState: {
- major: TransactionMajorState.Failed,
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://failed/withdrawal",
+ txState: {
+ major: TransactionMajorState.Failed,
+ },
},
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "normal payment",
- },
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "aborting in progress",
- },
- txState: {
- major: TransactionMajorState.Aborting,
- },
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "aborted payment",
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "normal payment",
+ },
},
- txState: {
- major: TransactionMajorState.Aborted,
- },
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "pending payment",
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "aborting in progress",
+ },
+ txState: {
+ major: TransactionMajorState.Aborting,
+ },
},
- txState: {
- major: TransactionMajorState.Pending,
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "aborted payment",
+ },
+ txState: {
+ major: TransactionMajorState.Aborted,
+ },
},
- },
- {
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: "failed payment",
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "pending payment",
+ },
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
- txState: {
- major: TransactionMajorState.Failed,
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "failed payment",
+ },
+ txState: {
+ major: TransactionMajorState.Failed,
+ },
},
- },
- exampleData.refund,
- exampleData.deposit,
- ],
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
flags: [],
@@ -389,15 +400,17 @@ export const SomeTransactionsInDifferentStates = tests.createExample(
export const SomeTransactionsWithTwoCurrencies = tests.createExample(
TestedComponent,
{
- transactions: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
- exampleData.refresh,
- exampleData.refund,
- exampleData.deposit,
- ],
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.refresh,
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
balances: [
{
flags: [],
@@ -431,7 +444,9 @@ export const SomeTransactionsWithTwoCurrencies = tests.createExample(
);
export const FiveOfficialCurrencies = tests.createExample(TestedComponent, {
- transactions: [exampleData.withdraw],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
flags: [],
@@ -505,7 +520,9 @@ export const FiveOfficialCurrencies = tests.createExample(TestedComponent, {
export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
TestedComponent,
{
- transactions: [exampleData.withdraw],
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
balances: [
{
flags: [],
@@ -578,12 +595,14 @@ export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
);
export const PeerToPeer = tests.createExample(TestedComponent, {
- transactions: [
- exampleData.pull_credit,
- exampleData.pull_debit,
- exampleData.push_credit,
- exampleData.push_debit,
- ],
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.pull_credit,
+ exampleData.pull_debit,
+ exampleData.push_credit,
+ exampleData.push_debit,
+ ],
+ },
balances: [
{
flags: [],
diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx
index 233bd8f28..f81e6db9f 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.tsx
@@ -20,7 +20,7 @@ import {
NotificationType,
ScopeType,
Transaction,
- WalletBalance
+ WalletBalance,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -45,31 +45,39 @@ import { Button } from "../mui/Button.js";
import { NoBalanceHelp } from "../popup/NoBalanceHelp.js";
import DownloadIcon from "../svg/download_24px.inline.svg";
import UploadIcon from "../svg/upload_24px.inline.svg";
+import { TextField } from "../mui/TextField.js";
+import { TextFieldHandler } from "../mui/handlers.js";
interface Props {
currency?: string;
+ search?: boolean;
goToWalletDeposit: (currency: string) => Promise<void>;
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
}
export function HistoryPage({
- currency,
+ currency: _c,
+ search: showSearch,
goToWalletManualWithdraw,
goToWalletDeposit,
}: Props): VNode {
const { i18n } = useTranslationContext();
const api = useBackendContext();
const [balanceIndex, setBalanceIndex] = useState<number>(0);
+ const [search, setSearch] = useState<string>();
+
const [settings] = useSettings();
const state = useAsyncAsHook(async () => {
- const b = await api.wallet.call(WalletApiOperation.GetBalances, {})
- const balance = b.balances.length > 0 ? b.balances[balanceIndex] : undefined
+ const b = await api.wallet.call(WalletApiOperation.GetBalances, {});
+ const balance =
+ b.balances.length > 0 ? b.balances[balanceIndex] : undefined;
const tx = await api.wallet.call(WalletApiOperation.GetTransactions, {
- scopeInfo: balance?.scopeInfo,
+ scopeInfo: showSearch ? undefined : balance?.scopeInfo,
sort: "descending",
includeRefreshes: settings.showRefeshTransactions,
- })
- return { b, tx }
- }, [balanceIndex]);
+ search,
+ });
+ return { b, tx };
+ }, [balanceIndex, search]);
useEffect(() => {
return api.listener.onUpdateNotification(
@@ -104,14 +112,48 @@ export function HistoryPage({
/>
);
}
+
+ const byDate = state.response.tx.transactions.reduce(
+ (rv, x) => {
+ const startDay =
+ x.timestamp.t_s === "never"
+ ? 0
+ : startOfDay(x.timestamp.t_s * 1000).getTime();
+ if (startDay) {
+ if (!rv[startDay]) {
+ rv[startDay] = [];
+ // datesWithTransaction.push(String(startDay));
+ }
+ rv[startDay].push(x);
+ }
+
+ return rv;
+ },
+ {} as { [x: string]: Transaction[] },
+ );
+
+ if (showSearch) {
+ return (
+ <FilteredHistoryView
+ search={{
+ value: search ?? "",
+ onInput: pushAlertOnError(async (d: string) => {
+ setSearch(d);
+ }),
+ }}
+ transactionsByDate={byDate}
+ />
+ );
+ }
+
return (
<HistoryView
balanceIndex={balanceIndex}
- changeBalanceIndex={b => setBalanceIndex(b)}
+ changeBalanceIndex={(b) => setBalanceIndex(b)}
balances={state.response.b.balances}
goToWalletManualWithdraw={goToWalletManualWithdraw}
goToWalletDeposit={goToWalletDeposit}
- transactions={state.response.tx.transactions}
+ transactionsByDate={byDate}
/>
);
}
@@ -120,15 +162,15 @@ export function HistoryView({
balances,
balanceIndex,
changeBalanceIndex,
- transactions,
+ transactionsByDate,
goToWalletManualWithdraw,
goToWalletDeposit,
}: {
- balanceIndex: number,
+ balanceIndex: number;
changeBalanceIndex: (s: number) => void;
goToWalletDeposit: (currency: string) => Promise<void>;
goToWalletManualWithdraw: (currency?: string) => Promise<void>;
- transactions: Transaction[];
+ transactionsByDate: Record<string, Transaction[]>;
balances: WalletBalance[];
}): VNode {
const { i18n } = useTranslationContext();
@@ -139,20 +181,7 @@ export function HistoryView({
? Amounts.jsonifyAmount(balance.available)
: undefined;
- const datesWithTransaction: string[] = [];
- const byDate = transactions.reduce((rv, x) => {
- const startDay =
- x.timestamp.t_s === "never" ? 0 : startOfDay(x.timestamp.t_s * 1000).getTime();
- if (startDay) {
- if (!rv[startDay]) {
- rv[startDay] = [];
- datesWithTransaction.push(String(startDay));
- }
- rv[startDay].push(x);
- }
-
- return rv;
- }, {} as { [x: string]: Transaction[] });
+ const datesWithTransaction: string[] = Object.keys(transactionsByDate);
return (
<Fragment>
@@ -163,62 +192,19 @@ export function HistoryView({
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
+ marginRight: 20,
}}
>
- <div
- style={{
- width: "fit-content",
- display: "flex",
- }}
- >
- {balances.length === 1 ? (
- <CenteredText style={{ fontSize: "x-large", margin: 8 }}>
- {balance.scopeInfo.currency}
- </CenteredText>
- ) : (
- <NiceSelect style={{ flexDirection: "column" }}>
- <select
- style={{
- fontSize: "x-large",
- }}
- value={balanceIndex}
- onChange={(e) => {
- changeBalanceIndex(Number.parseInt(e.currentTarget.value, 10));
- }}
- >
- {balances.map((entry, index) => {
- return (
- <option value={index} key={entry.scopeInfo.currency}>
- {entry.scopeInfo.currency}
- </option>
- );
- })}
- </select>
- <div style={{ fontSize: "small", color: "grey" }}>
- {balance.scopeInfo.type === ScopeType.Exchange || balance.scopeInfo.type === ScopeType.Auditor ? balance.scopeInfo.url : undefined}
- </div>
- </NiceSelect>
- )}
- {available && (
- <CenteredBoldText
- style={{
- display: "inline-block",
- fontSize: "x-large",
- margin: 8,
- }}
- >
- {Amounts.stringifyValue(available, 2)}
- </CenteredBoldText>
- )}
- </div>
<div>
<Button
tooltip="Transfer money to the wallet"
startIcon={DownloadIcon}
variant="contained"
- onClick={() => goToWalletManualWithdraw(balance.scopeInfo.currency)}
+ onClick={() =>
+ goToWalletManualWithdraw(balance.scopeInfo.currency)
+ }
>
- <i18n.Translate>Add</i18n.Translate>
+ <i18n.Translate>Receive</i18n.Translate>
</Button>
{available && Amounts.isNonZero(available) && (
<Button
@@ -232,6 +218,125 @@ export function HistoryView({
</Button>
)}
</div>
+ <div style={{ display: "flex", flexDirection: "column" }}>
+ <h3 style={{ marginBottom: 0 }}>Balance</h3>
+ <div
+ style={{
+ width: "fit-content",
+ display: "flex",
+ }}
+ >
+ {balances.length === 1 ? (
+ <CenteredText style={{ fontSize: "x-large", margin: 8 }}>
+ {balance.scopeInfo.currency}
+ </CenteredText>
+ ) : (
+ <NiceSelect style={{ flexDirection: "column" }}>
+ <select
+ style={{
+ fontSize: "x-large",
+ }}
+ value={balanceIndex}
+ onChange={(e) => {
+ changeBalanceIndex(
+ Number.parseInt(e.currentTarget.value, 10),
+ );
+ }}
+ >
+ {balances.map((entry, index) => {
+ return (
+ <option value={index} key={entry.scopeInfo.currency}>
+ {entry.scopeInfo.currency}
+ </option>
+ );
+ })}
+ </select>
+ <div style={{ fontSize: "small", color: "grey" }}>
+ {balance.scopeInfo.type === ScopeType.Exchange ||
+ balance.scopeInfo.type === ScopeType.Auditor
+ ? balance.scopeInfo.url
+ : undefined}
+ </div>
+ </NiceSelect>
+ )}
+ {available && (
+ <CenteredBoldText
+ style={{
+ display: "inline-block",
+ fontSize: "x-large",
+ margin: 8,
+ }}
+ >
+ {Amounts.stringifyValue(available, 2)}
+ </CenteredBoldText>
+ )}
+ </div>
+ </div>
+ </div>
+ </section>
+ {datesWithTransaction.length === 0 ? (
+ <section>
+ <i18n.Translate>
+ Your transaction history is empty for this currency.
+ </i18n.Translate>
+ </section>
+ ) : (
+ <section>
+ {datesWithTransaction.map((d, i) => {
+ return (
+ <Fragment key={i}>
+ <DateSeparator>
+ <Time
+ timestamp={AbsoluteTime.fromMilliseconds(
+ Number.parseInt(d, 10),
+ )}
+ format="dd MMMM yyyy"
+ />
+ </DateSeparator>
+ {transactionsByDate[d].map((tx, i) => (
+ <HistoryItem key={i} tx={tx} />
+ ))}
+ </Fragment>
+ );
+ })}
+ </section>
+ )}
+ </Fragment>
+ );
+}
+
+export function FilteredHistoryView({
+ search,
+ transactionsByDate,
+}: {
+ search: TextFieldHandler;
+ transactionsByDate: Record<string, Transaction[]>;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const datesWithTransaction: string[] = Object.keys(transactionsByDate);
+
+ return (
+ <Fragment>
+ <section>
+ <div
+ style={{
+ display: "flex",
+ flexWrap: "wrap",
+ alignItems: "center",
+ justifyContent: "space-between",
+ marginRight: 20,
+ }}
+ >
+ <TextField
+ label="Search"
+ variant="filled"
+ error={search.error}
+ required
+ fullWidth
+ value={search.value}
+ onChange={search.onInput}
+ />
</div>
</section>
{datesWithTransaction.length === 0 ? (
@@ -253,7 +358,7 @@ export function HistoryView({
format="dd MMMM yyyy"
/>
</DateSeparator>
- {byDate[d].map((tx, i) => (
+ {transactionsByDate[d].map((tx, i) => (
<HistoryItem key={i} tx={tx} />
))}
</Fragment>
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
index 4d045ee13..7b80977f3 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
@@ -23,13 +23,12 @@ import {
stringifyPaytoUri,
validateIban,
} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ErrorMessage } from "../../components/ErrorMessage.js";
-import { SelectList } from "../../components/SelectList.js";
-import { Input, SubTitle, SvgIcon } from "../../components/styled/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { SubTitle, SvgIcon } from "../../components/styled/index.js";
import { Button } from "../../mui/Button.js";
import { TextFieldHandler } from "../../mui/handlers.js";
import { TextField } from "../../mui/TextField.js";
@@ -110,6 +109,7 @@ export function ReadyView({
<div style={{ width: "100%", display: "flex" }}>
{Object.entries(accountType.list).map(([key, name], idx) => (
<div
+ key={idx}
style={{
marginLeft: 8,
padding: 8,
@@ -119,7 +119,7 @@ export function ReadyView({
accountType.value === key ? "#0042b2" : "unset",
color: accountType.value === key ? "white" : "unset",
}}
- onClick={(e) => {
+ onClick={() => {
if (accountType.onChange) {
accountType.onChange(key);
}
@@ -130,6 +130,7 @@ export function ReadyView({
))}
</div>
<div style={{ border: "1px solid gray", padding: 8, borderRadius: 5 }}>
+ --- {uri.value} ---
<p>
<CustomFieldByAccountType
type={accountType.value as AccountType}
@@ -431,7 +432,7 @@ function BitcoinAddressAccount({ field }: { field: TextFieldHandler }): VNode {
}
function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
- return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
+ return Object.keys(obj).some((k) => (obj as Record<string,unknown>)[k] !== undefined)
? obj
: undefined;
}
@@ -488,20 +489,21 @@ function TalerBankAddressAccount({
}
//Taken from libeufin and libeufin took it from the ISO20022 XSD schema
-const bicRegex = /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/;
-const ibanRegex = /^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$/;
+// const bicRegex = /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/;
+// const ibanRegex = /^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$/;
function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode {
const { i18n } = useTranslationContext();
- const [bic, setBic] = useState<string | undefined>(undefined);
+ // const [bic, setBic] = useState<string | undefined>(undefined);
const [iban, setIban] = useState<string | undefined>(undefined);
const [name, setName] = useState<string | undefined>(undefined);
- const errors = undefinedIfEmpty({
- bic: !bic
- ? undefined
- : !bicRegex.test(bic)
- ? i18n.str`Invalid bic`
- : undefined,
+ const bic = ""
+ const errorsFN = (iban:string | undefined, name: string | undefined) => undefinedIfEmpty({
+ // bic: !bic
+ // ? undefined
+ // : !bicRegex.test(bic)
+ // ? i18n.str`Invalid bic`
+ // : undefined,
iban: !iban
? i18n.str`Can't be empty`
: validateIban(iban).type === "invalid"
@@ -509,16 +511,20 @@ function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode {
: undefined,
name: !name ? i18n.str`Can't be empty` : undefined,
});
+ const errors = errorsFN(iban, name)
function sendUpdateIfNoErrors(
bic: string | undefined,
iban: string,
name: string,
): void {
- if (!errors && field.onInput) {
+ if (!field.onInput) return;
+ if (!errorsFN(iban, name)) {
const p = buildPayto("iban", iban, bic);
p.params["receiver-name"] = name;
field.onInput(stringifyPaytoUri(p));
+ } else {
+ field.onInput("")
}
}
return (
@@ -584,7 +590,7 @@ function CustomFieldByAccountType({
type: AccountType;
field: TextFieldHandler;
}): VNode {
- const { i18n } = useTranslationContext();
+ // const { i18n } = useTranslationContext();
const AccountForm = formComponentByAccountType[type];
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
index 999223fd8..a01ea6967 100644
--- a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
@@ -15,16 +15,19 @@
*/
import {
+ assertUnreachable,
parseTalerUri,
TalerUri,
+ TalerUriAction,
TranslatedString,
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { css } from "@linaria/core";
import { styled } from "@linaria/react";
import jsQR, * as pr from "jsqr";
-import { Fragment, h, VNode } from "preact";
+import { h, VNode } from "preact";
import { useRef, useState } from "preact/hooks";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
import { Alert } from "../mui/Alert.js";
import { Button } from "../mui/Button.js";
import { Grid } from "../mui/Grid.js";
@@ -182,7 +185,7 @@ async function createCanvasFromFile(
canvas.width = img.width;
canvas.height = img.height;
return new Promise<string | undefined>((ok, bad) => {
- img.addEventListener("load", (e) => {
+ img.addEventListener("load", () => {
try {
const code = drawIntoCanvasAndGetQR(img, canvas);
ok(code);
@@ -194,7 +197,7 @@ async function createCanvasFromFile(
}
async function waitUntilReady(video: HTMLVideoElement): Promise<void> {
- return new Promise((ok, bad) => {
+ return new Promise((ok, _bad) => {
if (video.readyState === video.HAVE_ENOUGH_DATA) {
return ok();
}
@@ -211,8 +214,25 @@ export function QrReaderPage({ onDetected }: Props): VNode {
const { i18n } = useTranslationContext();
+ function onChangeDetect(str: string) {
+ if (str) {
+ const uri = parseTalerUri(str);
+ if (!uri) {
+ setError(
+ i18n.str`URI is not valid. Taler URI should start with "taler://"`,
+ );
+ } else {
+ onDetected(uri);
+ setError(undefined);
+ }
+ } else {
+ setError(undefined);
+ }
+ setValue(str);
+ }
+
function onChange(str: string) {
- if (!!str) {
+ if (str) {
if (!parseTalerUri(str)) {
setError(
i18n.str`URI is not valid. Taler URI should start with "taler://"`,
@@ -244,7 +264,7 @@ export function QrReaderPage({ onDetected }: Props): VNode {
try {
const code = await createCanvasFromVideo(video, canvasRef.current);
if (code) {
- onChange(code);
+ onChangeDetect(code);
setShow("canvas");
}
stream.getTracks().forEach((e) => {
@@ -264,7 +284,7 @@ export function QrReaderPage({ onDetected }: Props): VNode {
try {
const code = await createCanvasFromFile(fileContent, canvasRef.current);
if (code) {
- onChange(code);
+ onChangeDetect(code);
setShow("canvas");
} else {
setError(i18n.str`Could not found a QR code in the file`);
@@ -273,8 +293,8 @@ export function QrReaderPage({ onDetected }: Props): VNode {
setError(i18n.str`something unexpected happen: ${error}`);
}
}
+ const uri = parseTalerUri(value);
- const active = value === "";
return (
<Container>
<section>
@@ -283,59 +303,75 @@ export function QrReaderPage({ onDetected }: Props): VNode {
Scan a QR code or enter taler:// URI below
</i18n.Translate>
</h1>
-
- <p>
- <TextField
- label="Taler URI"
- variant="standard"
- fullWidth
- value={value}
- onChange={onChange}
- />
- </p>
+ <div style={{ justifyContent: "space-between", display: "flex" }}>
+ <div style={{ width: "75%" }}>
+ <TextField
+ label="Taler URI"
+ variant="filled"
+ fullWidth
+ value={value}
+ onChange={onChange}
+ />
+ </div>
+ {uri && (
+ <Button
+ disabled={!!error}
+ variant="contained"
+ color="success"
+ onClick={async () => {
+ if (uri) onDetected(uri);
+ }}
+ >
+ {(function (talerUri: TalerUri): VNode {
+ switch (talerUri.type) {
+ case TalerUriAction.Pay:
+ return <i18n.Translate>Pay invoice</i18n.Translate>;
+ case TalerUriAction.Withdraw:
+ return (
+ <i18n.Translate>Withdrawal from bank</i18n.Translate>
+ );
+ case TalerUriAction.Refund:
+ return <i18n.Translate>Claim refund</i18n.Translate>;
+ case TalerUriAction.PayPull:
+ return <i18n.Translate>Pay invoice</i18n.Translate>;
+ case TalerUriAction.PayPush:
+ return <i18n.Translate>Accept payment</i18n.Translate>;
+ case TalerUriAction.PayTemplate:
+ return <i18n.Translate>Complete order</i18n.Translate>;
+ case TalerUriAction.Restore:
+ return <i18n.Translate>Restore wallet</i18n.Translate>;
+ case TalerUriAction.DevExperiment:
+ return <i18n.Translate>Enable experiment</i18n.Translate>;
+ case TalerUriAction.WithdrawExchange:
+ return (
+ <i18n.Translate>Withdraw from exchange</i18n.Translate>
+ );
+ case TalerUriAction.AddExchange:
+ return <i18n.Translate>Add exchange</i18n.Translate>;
+ default: {
+ assertUnreachable(talerUri);
+ }
+ }
+ })(uri)}
+ </Button>
+ )}
+ </div>
<Grid container justifyContent="space-around" columns={2}>
<Grid item xs={2}>
<p>{error && <Alert severity="error">{error}</Alert>}</p>
</Grid>
- <Grid item xs={1}>
- {!active && (
- <Button
- variant="contained"
- onClick={async () => {
- setShow("nothing");
- onChange("");
- }}
- color="error"
- >
- <i18n.Translate>Clear</i18n.Translate>
- </Button>
- )}
- </Grid>
- <Grid item xs={1}>
- {value && (
- <Button
- disabled={!!error}
- variant="contained"
- color="success"
- onClick={async () => {
- const uri = parseTalerUri(value);
- if (uri) onDetected(uri);
- }}
- >
- <i18n.Translate>Open</i18n.Translate>
- </Button>
- )}
- </Grid>
- <Grid item xs={1}>
- <InputFile onChange={onFileRead}>Read QR from file</InputFile>
- </Grid>
- <Grid item xs={1}>
+ <Grid item xs={2}>
<p>
<Button variant="contained" onClick={startVideo}>
Use Camera
</Button>
</p>
</Grid>
+ <EnabledBySettings name="advancedMode">
+ <Grid item xs={2}>
+ <InputFile onChange={onFileRead}>Read QR from file</InputFile>
+ </Grid>
+ </EnabledBySettings>
</Grid>
</section>
<div>
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
index 34dd24cea..0d0a31a2d 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
@@ -159,7 +159,7 @@ export function SettingsView({
<WarningBox>
<i18n.Translate>
The version of wallet core is not supported. (supported
- version: {WALLET_CORE_SUPPORTED_VERSION})
+ version: {WALLET_CORE_SUPPORTED_VERSION}, wallet version: {coreVersion.version})
</i18n.Translate>
</WarningBox>
)}
@@ -274,6 +274,7 @@ function AdvanceSettings(): VNode {
<Checkbox
label={label}
name={name}
+ key={name}
description={description}
enabled={settings[settingsName]}
onToggle={async () => {
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 10ca67663..1f0293352 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -19,6 +19,7 @@ import {
AmountJson,
Amounts,
AmountString,
+ DenomLossEventType,
MerchantInfo,
NotificationType,
OrderShortInfo,
@@ -230,72 +231,75 @@ function TransactionTemplate({
<Fragment>
<section style={{ padding: 8, textAlign: "center" }}>
{transaction?.error &&
- // FIXME: wallet core should stop sending this error on KYC
- transaction.error.code !==
+ // FIXME: wallet core should stop sending this error on KYC
+ transaction.error.code !==
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED ? (
<ErrorAlertView
error={alertFromError(
i18n,
- i18n.str`There was an error trying to complete the transaction`,
+ i18n.str`There was an error trying to complete the transaction.`,
transaction.error,
)}
/>
) : undefined}
- {transaction.txState.minor === TransactionMinorState.KycRequired && (
- <AlertView
- alert={{
- type: "warning",
- message: i18n.str`KYC check required for the transaction to complete`,
- description:
- transaction.kycUrl && typeof transaction.kycUrl === "string" ? (
- <div>
- <i18n.Translate>
- Follow this link to the{` `}
- <a
- rel="noreferrer"
- target="_bank"
- href={transaction.kycUrl}
- >
- KYC verifier
- </a>
- </i18n.Translate>
- </div>
- ) : (
- i18n.str`No more information has been provided`
- ),
- }}
- />
- )}
- {transaction.txState.minor === TransactionMinorState.AmlRequired && (
- <WarningBox>
- <i18n.Translate>
- The transaction has been blocked since the account required an AML
- check
- </i18n.Translate>
- </WarningBox>
- )}
- {transaction.txState.major === TransactionMajorState.Pending && (
- <WarningBox>
- <div style={{ justifyContent: "center", lineHeight: "25px" }}>
- <i18n.Translate>This transaction is not completed</i18n.Translate>
- <Link onClick={onRetry} style={{padding: 0}}>
- <SvgIcon
- title={i18n.str`Retry`}
- dangerouslySetInnerHTML={{ __html: refreshIcon }}
- color="black"
- />
- </Link>
- </div>
- </WarningBox>
- )}
+ {transaction.txState.major === TransactionMajorState.Pending &&
+ (transaction.txState.minor === TransactionMinorState.KycRequired ? (
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`KYC check required for the transaction to complete.`,
+ description:
+ transaction.kycUrl &&
+ typeof transaction.kycUrl === "string" ? (
+ <div>
+ <i18n.Translate>
+ Follow this link to the{` `}
+ <a
+ rel="noreferrer"
+ target="_bank"
+ href={transaction.kycUrl}
+ >
+ KYC verifier.
+ </a>
+ </i18n.Translate>
+ </div>
+ ) : (
+ i18n.str`No additional information has been provided.`
+ ),
+ }}
+ />
+ ) : transaction.txState.minor ===
+ TransactionMinorState.AmlRequired ? (
+ <WarningBox>
+ <i18n.Translate>
+ The transaction has been blocked since the account required an
+ AML check.
+ </i18n.Translate>
+ </WarningBox>
+ ) : (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This transaction is not completed
+ </i18n.Translate>
+ <Link onClick={onRetry} style={{ padding: 0 }}>
+ <SvgIcon
+ title={i18n.str`Retry`}
+ dangerouslySetInnerHTML={{ __html: refreshIcon }}
+ color="black"
+ />
+ </Link>
+ </div>
+ </WarningBox>
+ ))}
{transaction.txState.major === TransactionMajorState.Aborted && (
<InfoBox>
- <i18n.Translate>This transaction was aborted</i18n.Translate>
+ <i18n.Translate>This transaction was aborted.</i18n.Translate>
</InfoBox>
)}
{transaction.txState.major === TransactionMajorState.Failed && (
<ErrorBox>
- <i18n.Translate>This transaction failed</i18n.Translate>
+ <i18n.Translate>This transaction failed.</i18n.Translate>
</ErrorBox>
)}
{confirmBeforeForget ? (
@@ -426,7 +430,7 @@ export function TransactionView({
transaction,
onDelete,
onAbort,
- onBack,
+ // onBack,
onResume,
onSuspend,
onRetry,
@@ -443,10 +447,13 @@ export function TransactionView({
transaction.type === TransactionType.Withdrawal ||
transaction.type === TransactionType.InternalWithdrawal
) {
- const conversion =
- transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
- ? transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
- : [];
+ // const conversion =
+ // transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ // ? transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ // : [];
+ const blockedByKycOrAml =
+ transaction.txState.minor === TransactionMinorState.KycRequired ||
+ transaction.txState.minor === TransactionMinorState.AmlRequired;
return (
<TransactionTemplate
transaction={transaction}
@@ -466,30 +473,32 @@ export function TransactionView({
{transaction.exchangeBaseUrl}
</Header>
- {transaction.txState.major !==
- TransactionMajorState.Pending ? undefined : transaction.txState
- .minor === TransactionMinorState.KycRequired ||
- transaction.txState.minor ===
- TransactionMinorState.AmlRequired ? undefined : transaction
- .withdrawalDetails.type === WithdrawalType.ManualTransfer
- && transaction.withdrawalDetails.exchangeCreditAccountDetails ? (
+ {transaction.txState.major !== TransactionMajorState.Pending ||
+ blockedByKycOrAml ? undefined : transaction.withdrawalDetails.type ===
+ WithdrawalType.ManualTransfer &&
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ? (
<Fragment>
<InfoBox>
- {transaction.withdrawalDetails.exchangeCreditAccountDetails.length > 1 ?
+ {transaction.withdrawalDetails.exchangeCreditAccountDetails
+ .length > 1 ? (
<span>
<i18n.Translate>
- Now the payment service provider is waiting for <Amount value={raw} /> to
- be transferred. Select one of the accounts and use the information below
- to complete the operation by making a wire transfer from your bank account.
+ Now the payment service provider is waiting for{" "}
+ <Amount value={raw} /> to be transferred. Select one of the
+ accounts and use the information below to complete the
+ operation by making a wire transfer from your bank account.
</i18n.Translate>
</span>
- :
- <span><i18n.Translate>
- Now the payment service provider is waiting for <Amount value={raw} /> to
- be transferred. Use the information below to complete the operation
- by making a wire transfer from your bank account.
- </i18n.Translate></span>}
-
+ ) : (
+ <span>
+ <i18n.Translate>
+ Now the payment service provider is waiting for{" "}
+ <Amount value={raw} /> to be transferred. Use the
+ information below to complete the operation by making a wire
+ transfer from your bank account.
+ </i18n.Translate>
+ </span>
+ )}
</InfoBox>
<BankDetailsByPaytoType
amount={raw}
@@ -581,6 +590,7 @@ export function TransactionView({
format="dd MMMM yyyy"
/>
}
+ .
</i18n.Translate>
</td>
</tr>
@@ -649,11 +659,11 @@ export function TransactionView({
price={getAmountWithFee(effective, raw, "debit")}
effectiveRefund={effectiveRefund}
info={transaction.info}
- proposalId={transaction.proposalId}
/>
}
kind="neutral"
/>
+ <ShowFullContractTermPopup transactionId={transaction.transactionId} />
</TransactionTemplate>
);
}
@@ -695,7 +705,7 @@ export function TransactionView({
/>
{!shouldBeWired ? (
<Part
- title={i18n.str`Wire transfer deadline`}
+ title={i18n.str`Wire transfer deadline.`}
text={
<Time timestamp={wireTime} format="dd MMMM yyyy 'at' HH:mm" />
}
@@ -705,7 +715,7 @@ export function TransactionView({
<AlertView
alert={{
type: "warning",
- message: i18n.str`Wire transfer is not initiated`,
+ message: i18n.str`Wire transfer is not initiated.`,
description: i18n.str` `,
}}
/>
@@ -714,7 +724,7 @@ export function TransactionView({
<AlertView
alert={{
type: "success",
- message: i18n.str`Wire transfer completed`,
+ message: i18n.str`Wire transfer completed.`,
description: i18n.str` `,
}}
/>
@@ -732,7 +742,7 @@ export function TransactionView({
<AlertView
alert={{
type: "info",
- message: i18n.str`Wire transfer in progress`,
+ message: i18n.str`Wire transfer in progress.`,
description: i18n.str` `,
}}
/>
@@ -1026,10 +1036,110 @@ export function TransactionView({
);
}
- if (transaction.type === TransactionType.Recoup) {
- throw Error("recoup transaction not implemented");
+ if (transaction.type === TransactionType.DenomLoss) {
+ switch (transaction.lossEventType) {
+ case DenomLossEventType.DenomExpired: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination expired.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ case DenomLossEventType.DenomVanished: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination vanished.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ case DenomLossEventType.DenomUnoffered: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination is unoffered.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ default: {
+ assertUnreachable(transaction.lossEventType);
+ }
+ }
}
- if (transaction.type === TransactionType.Reward) {
+ if (transaction.type === TransactionType.Recoup) {
throw Error("recoup transaction not implemented");
}
assertUnreachable(transaction);
@@ -1075,127 +1185,6 @@ export function MerchantDetails({
);
}
-// function DeliveryDetails({
-// date,
-// location,
-// }: {
-// date: TalerProtocolTimestamp | undefined;
-// location: Location | undefined;
-// }): VNode {
-// const { i18n } = useTranslationContext();
-// return (
-// <PurchaseDetailsTable>
-// {location && (
-// <Fragment>
-// {location.country && (
-// <tr>
-// <td>
-// <i18n.Translate>Country</i18n.Translate>
-// </td>
-// <td>{location.country}</td>
-// </tr>
-// )}
-// {location.address_lines && (
-// <tr>
-// <td>
-// <i18n.Translate>Address lines</i18n.Translate>
-// </td>
-// <td>{location.address_lines}</td>
-// </tr>
-// )}
-// {location.building_number && (
-// <tr>
-// <td>
-// <i18n.Translate>Building number</i18n.Translate>
-// </td>
-// <td>{location.building_number}</td>
-// </tr>
-// )}
-// {location.building_name && (
-// <tr>
-// <td>
-// <i18n.Translate>Building name</i18n.Translate>
-// </td>
-// <td>{location.building_name}</td>
-// </tr>
-// )}
-// {location.street && (
-// <tr>
-// <td>
-// <i18n.Translate>Street</i18n.Translate>
-// </td>
-// <td>{location.street}</td>
-// </tr>
-// )}
-// {location.post_code && (
-// <tr>
-// <td>
-// <i18n.Translate>Post code</i18n.Translate>
-// </td>
-// <td>{location.post_code}</td>
-// </tr>
-// )}
-// {location.town_location && (
-// <tr>
-// <td>
-// <i18n.Translate>Town location</i18n.Translate>
-// </td>
-// <td>{location.town_location}</td>
-// </tr>
-// )}
-// {location.town && (
-// <tr>
-// <td>
-// <i18n.Translate>Town</i18n.Translate>
-// </td>
-// <td>{location.town}</td>
-// </tr>
-// )}
-// {location.district && (
-// <tr>
-// <td>
-// <i18n.Translate>District</i18n.Translate>
-// </td>
-// <td>{location.district}</td>
-// </tr>
-// )}
-// {location.country_subdivision && (
-// <tr>
-// <td>
-// <i18n.Translate>Country subdivision</i18n.Translate>
-// </td>
-// <td>{location.country_subdivision}</td>
-// </tr>
-// )}
-// </Fragment>
-// )}
-
-// {!location || !date ? undefined : (
-// <tr>
-// <td colSpan={2}>
-// <hr />
-// </td>
-// </tr>
-// )}
-// {date && (
-// <Fragment>
-// <tr>
-// <td>
-// <i18n.Translate>Date</i18n.Translate>
-// </td>
-// <td>
-// <Time
-// timestamp={AbsoluteTime.fromProtocolTimestamp(date)}
-// format="dd MMMM yyyy, HH:mm"
-// />
-// </td>
-// </tr>
-// </Fragment>
-// )}
-// </PurchaseDetailsTable>
-// );
-// }
-
export function ExchangeDetails({ exchange }: { exchange: string }): VNode {
return (
<div>
@@ -1255,28 +1244,30 @@ export function InvoiceCreationDetails({
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1300,28 +1291,30 @@ export function InvoicePaymentDetails({
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.value} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1345,28 +1338,30 @@ export function TransferCreationDetails({
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Transfer</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Transfer</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1390,31 +1385,34 @@ export function TransferPickupDetails({
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
+
export function WithdrawDetails({
conversion,
amount,
@@ -1424,12 +1422,6 @@ export function WithdrawDetails({
}): VNode {
const { i18n } = useTranslationContext();
- const maxFrac = [amount.fee, amount.fee]
- .map((a) => Amounts.maxFractionalDigits(a))
- .reduce((c, p) => Math.max(c, p), 0);
-
- const total = Amounts.add(amount.value, amount.fee).amount;
-
return (
<PurchaseDetailsTable>
{conversion ? (
@@ -1443,7 +1435,7 @@ export function WithdrawDetails({
</td>
</tr>
{conversion.fraction === amount.value.fraction &&
- conversion.value === amount.value.value ? undefined : (
+ conversion.value === amount.value.value ? undefined : (
<tr>
<td>
<i18n.Translate>Converted</i18n.Translate>
@@ -1465,28 +1457,30 @@ export function WithdrawDetails({
</tr>
)}
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1494,27 +1488,16 @@ export function WithdrawDetails({
export function PurchaseDetails({
price,
effectiveRefund,
- info,
- proposalId,
+ info: _info,
}: {
price: AmountWithFee;
effectiveRefund?: AmountJson;
info: OrderShortInfo;
- proposalId: string;
}): VNode {
const { i18n } = useTranslationContext();
const total = Amounts.add(price.value, price.fee).amount;
- // const hasProducts = info.products && info.products.length > 0;
-
- // const hasShipping =
- // info.delivery_date !== undefined || info.delivery_location !== undefined;
-
- const showLargePic = (): void => {
- return;
- };
-
return (
<PurchaseDetailsTable>
<tr>
@@ -1526,69 +1509,72 @@ export function PurchaseDetails({
</td>
</tr>
{Amounts.isNonZero(price.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={price.fee} />
- </td>
- </tr>
- )}
- {effectiveRefund && Amounts.isNonZero(effectiveRefund) ? (
- <Fragment>
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Subtotal</i18n.Translate>
- </td>
- <td>
- <Amount value={price.total} />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Refunded</i18n.Translate>
- </td>
- <td>
- <Amount value={effectiveRefund} negative />
- </td>
- </tr>
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={Amounts.sub(total, effectiveRefund).amount} />
- </td>
- </tr>
- </Fragment>
- ) : (
<Fragment>
<tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
<td>
- <i18n.Translate>Total</i18n.Translate>
+ <i18n.Translate>Fees</i18n.Translate>
</td>
<td>
- <Amount value={price.value} />
+ <Amount value={price.fee} />
</td>
</tr>
+ {effectiveRefund && Amounts.isNonZero(effectiveRefund) ? (
+ <Fragment>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Subtotal</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.total} />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Refunded</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={effectiveRefund} negative />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={Amounts.sub(total, effectiveRefund).amount} />
+ </td>
+ </tr>
+ </Fragment>
+ ) : (
+ <Fragment>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.value} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
</Fragment>
)}
+
{/* {hasProducts && (
<tr>
<td colSpan={2}>
@@ -1634,11 +1620,6 @@ export function PurchaseDetails({
</td>
</tr>
)} */}
- <tr>
- <td>
- <ShowFullContractTermPopup proposalId={proposalId} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1658,28 +1639,30 @@ function RefundDetails({ amount }: { amount: AmountWithFee }): VNode {
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1740,7 +1723,7 @@ function TrackingDepositDetails({
</tr>
{wireTransfers.map((wire) => (
- <tr>
+ <tr key={wire.id}>
<td>{wire.id}</td>
<td>
<Amount value={wire.amount} />
@@ -1750,6 +1733,7 @@ function TrackingDepositDetails({
</PurchaseDetailsTable>
);
}
+
function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
const { i18n } = useTranslationContext();
@@ -1765,28 +1749,30 @@ function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
</tr>
{Amounts.isNonZero(amount.fee) && (
- <tr>
- <td>
- <i18n.Translate>Fees</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
)}
- <tr>
- <td colSpan={2}>
- <hr />
- </td>
- </tr>
- <tr>
- <td>
- <i18n.Translate>Total</i18n.Translate>
- </td>
- <td>
- <Amount value={amount.total} maxFracSize={amount.maxFrac} />
- </td>
- </tr>
</PurchaseDetailsTable>
);
}
@@ -1976,8 +1962,9 @@ function ShowWithdrawalDetailForBankIntegrated({
if (
transaction.txState.major !== TransactionMajorState.Pending ||
transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
- )
+ ) {
return <Fragment />;
+ }
const raw = Amounts.parseOrThrow(transaction.amountRaw);
return (
<Fragment>
@@ -1989,7 +1976,7 @@ function ShowWithdrawalDetailForBankIntegrated({
setShowDetails(!showDetails);
}}
>
- show details
+ Show details.
</a>
</EnabledBySettings>
@@ -2003,7 +1990,7 @@ function ShowWithdrawalDetailForBankIntegrated({
/>
)}
{!transaction.withdrawalDetails.confirmed &&
- transaction.withdrawalDetails.bankConfirmationUrl ? (
+ transaction.withdrawalDetails.bankConfirmationUrl ? (
<InfoBox>
<div style={{ display: "block" }}>
<i18n.Translate>
@@ -2026,7 +2013,7 @@ function ShowWithdrawalDetailForBankIntegrated({
<InfoBox>
<i18n.Translate>
Bank has confirmed the wire transfer. Waiting for the exchange to
- send the coins
+ send the coins.
</i18n.Translate>
</InfoBox>
)}
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index 495f015ff..4394a982f 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -31,8 +31,7 @@ import {
TalerError,
TalerErrorCode,
TalerErrorDetail,
- WalletDiagnostics,
- WalletNotification,
+ WalletNotification
} from "@gnu-taler/taler-util";
import {
WalletCoreApiClient,
@@ -47,6 +46,7 @@ import {
MessageFromFrontendWallet,
} from "./platform/api.js";
import { platform } from "./platform/foreground.js";
+import { WalletActivityTrack } from "./wxBackend.js";
/**
*
@@ -55,7 +55,7 @@ import { platform } from "./platform/foreground.js";
const logger = new Logger("wxApi");
-export const WALLET_CORE_SUPPORTED_VERSION = "1:0:0"
+export const WALLET_CORE_SUPPORTED_VERSION = "4:0:0"
export interface ExtendedPermissionsResponse {
newValue: boolean;
@@ -75,8 +75,10 @@ export interface BackgroundOperations {
response: void;
};
getNotifications: {
- request: void;
- response: WalletEvent[];
+ request: {
+ filter: string;
+ };
+ response: WalletActivityTrack[];
};
clearNotifications: {
request: void;
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index 44e4c0d48..5fa255f5d 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -27,11 +27,14 @@ import {
AbsoluteTime,
LogLevel,
Logger,
+ NotificationType,
OpenedPromise,
SetTimeoutTimerAPI,
TalerError,
TalerErrorCode,
TalerErrorDetail,
+ TransactionMinorState,
+ WalletNotification,
getErrorDetailFromException,
makeErrorDetail,
openPromise,
@@ -43,17 +46,18 @@ import {
DbAccess,
SynchronousCryptoWorkerFactoryPlain,
Wallet,
+ WalletApiOperation,
WalletOperations,
WalletStoresV1,
deleteTalerDatabase,
exportDb,
importDb,
} from "@gnu-taler/taler-wallet-core";
+import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
import { MessageFromFrontend, MessageResponse } from "./platform/api.js";
import { platform } from "./platform/background.js";
import { ExtensionOperations } from "./taler-wallet-interaction-loader.js";
-import { BackgroundOperations, WalletEvent } from "./wxApi.js";
-import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
+import { BackgroundOperations } from "./wxApi.js";
/**
* Currently active wallet instance. Might be unloaded and
@@ -65,11 +69,6 @@ let currentWallet: Wallet | undefined;
let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
-/**
- * Last version of an outdated DB, if applicable.
- */
-let outdatedDbVersion: number | undefined;
-
const walletInit: OpenedPromise<void> = openPromise<void>();
const logger = new Logger("wxBackend.ts");
@@ -91,16 +90,163 @@ async function resetDb(): Promise<void> {
await reinitWallet();
}
+export type WalletActivityTrack = {
+ id: number;
+ events: (WalletNotification & {when: AbsoluteTime})[];
+ start: AbsoluteTime;
+ type: NotificationType;
+ end: AbsoluteTime;
+ groupId: string;
+};
+
+let counter = 0;
+function getUniqueId(): number {
+ return counter++;
+}
+
//FIXME: maybe circular buffer
-const notifications: WalletEvent[] = []
-async function getNotifications(): Promise<WalletEvent[]> {
- return notifications
+const activity: WalletActivityTrack[] = [];
+
+function addNewWalletActivityNotification(list: WalletActivityTrack[], n: WalletNotification) {
+ const start = AbsoluteTime.now();
+ const ev = {...n, when:start};
+ switch (n.type) {
+ case NotificationType.BalanceChange: {
+ const groupId = `${n.type}:${n.hintTransactionId}`;
+ const found = list.find((a)=>a.groupId === groupId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(ev)
+ return;
+ }
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.BackupOperationError: {
+ const groupId = "";
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.TransactionStateTransition: {
+ const groupId = `${n.type}:${n.transactionId}`;
+ const found = list.find((a)=>a.groupId === groupId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(ev)
+ return;
+ }
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.WithdrawalOperationTransition: {
+ return;
+ }
+ case NotificationType.ExchangeStateTransition: {
+ const groupId = `${n.type}:${n.exchangeBaseUrl}`;
+ const found = list.find((a)=>a.groupId === groupId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(ev)
+ return;
+ }
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.Idle: {
+ const groupId = "";
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.TaskObservabilityEvent: {
+ const groupId = `${n.type}:${n.taskId}`;
+ const found = list.find((a)=>a.groupId === groupId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(ev)
+ return;
+ }
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ case NotificationType.RequestObservabilityEvent: {
+ const groupId = `${n.type}:${n.operation}:${n.requestId}`;
+ const found = list.find((a)=>a.groupId === groupId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(ev)
+ return;
+ }
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
+ }
+ }
}
-async function clearNotifications(): Promise<void> {
- notifications.splice(0, notifications.length)
+async function getNotifications({
+ filter,
+}: {
+ filter: string;
+}): Promise<WalletActivityTrack[]> {
+ if (!filter) return activity;
+
+ const rg = new RegExp(`.*${filter}.*`);
+ return activity.filter((event) => {
+ return rg.test(event.groupId.toLowerCase());
+ });
}
+async function clearNotifications(): Promise<void> {
+ activity.splice(0, activity.length);
+}
async function runGarbageCollector(): Promise<void> {
const dbBeforeGc = currentDatabase;
@@ -229,8 +375,10 @@ async function dispatch<
case "wallet": {
const w = currentWallet;
if (!w) {
- const lastError: TalerErrorDetail = walletInit.lastError instanceof TalerError ?
- walletInit.lastError.errorDetail : undefined
+ const lastError: TalerErrorDetail =
+ walletInit.lastError instanceof TalerError
+ ? walletInit.lastError.errorDetail
+ : undefined;
return {
type: "error",
@@ -239,16 +387,22 @@ async function dispatch<
error: makeErrorDetail(
TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
{ lastError },
- `wallet core not available${!lastError ? "" : `,last error: ${lastError.hint}`}`,
+ `wallet core not available${
+ !lastError ? "" : `,last error: ${lastError.hint}`
+ }`,
),
};
}
//multiple client can create the same id, send the wallet an unique key
- const newId = `${req.id}_${nextMessageIndex}`
- const resp = await w.handleCoreApiRequest(req.operation, newId, req.payload);
+ const newId = `${req.id}_${nextMessageIndex}`;
+ const resp = await w.handleCoreApiRequest(
+ req.operation,
+ newId,
+ req.payload,
+ );
//return to the client the original id
- resp.id = req.id
- return resp
+ resp.id = req.id;
+ return resp;
}
}
@@ -267,7 +421,7 @@ async function dispatch<
async function reinitWallet(): Promise<void> {
if (currentWallet) {
- currentWallet.stop();
+ await currentWallet.client.call(WalletApiOperation.Shutdown, {});
currentWallet = undefined;
}
currentDatabase = undefined;
@@ -305,11 +459,12 @@ async function reinitWallet(): Promise<void> {
config: {
testing: {
emitObservabilityEvents: settings.showWalletActivity,
+ devModeActive: settings.advancedMode,
},
features: {
allowHttp: settings.walletAllowHttp,
},
- }
+ },
});
} catch (e) {
logger.error("could not initialize wallet", e);
@@ -318,28 +473,23 @@ async function reinitWallet(): Promise<void> {
}
wallet.addNotificationListener((message) => {
if (settings.showWalletActivity) {
- notifications.push({
- notification: message,
- when: AbsoluteTime.now()
- })
+ addNewWalletActivityNotification(activity, message);
}
+ processWalletNotification(message);
+
platform.sendMessageToAllChannels({
type: "wallet",
notification: message,
});
});
- platform.keepAlive(() => {
- return wallet.runTaskLoop().catch((e) => {
- logger.error("error during wallet task loop", e);
- });
- });
// Useful for debugging in the background page.
if (typeof window !== "undefined") {
(window as any).talerWallet = wallet;
}
currentWallet = wallet;
+ updateIconBasedOnBalance();
return walletInit.resolve();
}
@@ -377,3 +527,46 @@ export async function wxMain(): Promise<void> {
console.error(e);
}
}
+
+async function updateIconBasedOnBalance() {
+ const balance = await currentWallet?.client.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ if (balance) {
+ let showAlert = false;
+ for (const b of balance.balances) {
+ if (b.flags.length > 0) {
+ console.log("b.flags", JSON.stringify(b.flags));
+ showAlert = true;
+ break;
+ }
+ }
+
+ if (showAlert) {
+ platform.setAlertedIcon();
+ } else {
+ platform.setNormalIcon();
+ }
+ }
+}
+
+/**
+ * All the actions triggered by notification that need to be
+ * run in the background.
+ *
+ * @param message
+ */
+async function processWalletNotification(message: WalletNotification) {
+ if (
+ message.type === NotificationType.TransactionStateTransition &&
+ (message.newTxState.minor === TransactionMinorState.KycRequired ||
+ message.oldTxState.minor === TransactionMinorState.KycRequired ||
+ message.newTxState.minor === TransactionMinorState.AmlRequired ||
+ message.oldTxState.minor === TransactionMinorState.AmlRequired ||
+ message.newTxState.minor === TransactionMinorState.BankConfirmTransfer ||
+ message.oldTxState.minor === TransactionMinorState.BankConfirmTransfer)
+ ) {
+ await updateIconBasedOnBalance();
+ }
+}
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index 08d0bb077..369b872b6 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -1,6 +1,6 @@
{
"name": "@gnu-taler/web-util",
- "version": "0.10.0",
+ "version": "0.10.7",
"description": "Generic helper functionality for GNU Taler Web Apps",
"type": "module",
"types": "./lib/index.node.d.ts",
diff --git a/packages/web-util/src/components/Button.tsx b/packages/web-util/src/components/Button.tsx
index ea0ea2f38..b142114e7 100644
--- a/packages/web-util/src/components/Button.tsx
+++ b/packages/web-util/src/components/Button.tsx
@@ -14,36 +14,56 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, OperationFail, OperationOk, OperationResult, TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ OperationAlternative,
+ OperationFail,
+ OperationOk,
+ OperationResult,
+ TalerError,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
// import { NotificationMessage, notifyInfo } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { HTMLAttributes, useEffect, useState, useTransition } from "preact/compat";
-import { NotificationMessage, buildUnifiedRequestErrorMessage, notifyInfo, useTranslationContext } from "../index.browser.js";
+import { HTMLAttributes, useState } from "preact/compat";
+import {
+ NotificationMessage,
+ buildUnifiedRequestErrorMessage,
+ notifyInfo,
+ useTranslationContext,
+} from "../index.browser.js";
// import { useBankCoreApiContext } from "../context/config.js";
// function errorMap<T extends OperationFail<unknown>>(resp: T, map: (d: T["case"]) => TranslatedString): void {
+export type OnOperationSuccesReturnType<T> = (
+ result: T extends OperationOk<any> ? T : never,
+) => TranslatedString | void;
+export type OnOperationFailReturnType<T> = (
+ (d: (T extends OperationFail<any> ? T : never) | (T extends OperationAlternative<any,any> ? T : never)) => TranslatedString)
+
export interface ButtonHandler<T extends OperationResult<A, B>, A, B> {
- onClick: () => Promise<T | undefined>,
+ onClick: () => Promise<T | undefined>;
onNotification: (n: NotificationMessage) => void;
- onOperationSuccess: ((result: T extends OperationOk<any> ? T : never) => void) | ((result: T extends OperationOk<any> ? T : never) => TranslatedString | undefined),
- onOperationFail: (d: T extends OperationFail<any> ? T : never) => TranslatedString;
+ onOperationSuccess: OnOperationSuccesReturnType<T>;
+ onOperationFail?: OnOperationFailReturnType<T>;
onOperationComplete?: () => void;
}
-interface Props<T extends OperationResult<A, B>, A, B> extends HTMLAttributes<HTMLButtonElement> {
- handler: ButtonHandler<T, A, B> | undefined,
+interface Props<T extends OperationResult<A, B>, A, B>
+ extends HTMLAttributes<HTMLButtonElement> {
+ handler: ButtonHandler<T, A, B> | undefined;
}
/**
* This button accept an async function and report a notification
* on error or success.
- *
+ *
* When the async function is running the inner text will change into
* a "loading" animation.
- *
- * @param param0
- * @returns
+ *
+ * @param param0
+ * @returns
*/
export function Button<T extends OperationResult<A, B>, A, B>({
handler,
@@ -53,69 +73,84 @@ export function Button<T extends OperationResult<A, B>, A, B>({
...rest
}: Props<T, A, B>): VNode {
const { i18n } = useTranslationContext();
- const [running, setRunning] = useState(false)
- return <button {...rest} disabled={disabled || running} onClick={(e) => {
- e.preventDefault();
- if (!handler) { return; }
- setRunning(true)
- handler.onClick().then((resp) => {
- if (resp) {
- if (resp.type === "ok") {
- const result: OperationOk<any> = resp
- // @ts-expect-error this is an operationOk
- const msg = handler.onOperationSuccess(result)
- if (msg) {
- notifyInfo(msg)
- }
+ const [running, setRunning] = useState(false);
+ return (
+ <button
+ {...rest}
+ disabled={disabled || running}
+ onClick={(e) => {
+ e.preventDefault();
+ if (!handler) {
+ return;
}
- if (resp.type === "fail") {
- // @ts-expect-error this is an operationFail
- const error: OperationFail<any> = resp;
- // @ts-expect-error this is an operationFail
- const title = handler.onOperationFail(error)
- handler.onNotification({
- title,
- type: "error",
- description: error.detail.hint as TranslatedString,
- debug: error.detail,
- when: AbsoluteTime.now(),
+ setRunning(true);
+ handler
+ .onClick()
+ .then((resp) => {
+ if (resp) {
+ if (resp.type === "ok") {
+ const result: OperationOk<any> = resp;
+ // @ts-expect-error this is an operationOk
+ const msg = handler.onOperationSuccess(result);
+ if (msg) {
+ notifyInfo(msg);
+ }
+ }
+ if (resp.type === "fail") {
+ const d = 'detail' in resp ? resp.detail : undefined
+
+ const title = !handler.onOperationFail ? "Unexpected error." as TranslatedString : handler.onOperationFail(resp as any);
+ handler.onNotification({
+ title,
+ type: "error",
+ description: d && d.hint ? d.hint as TranslatedString : undefined,
+ debug: d,
+ when: AbsoluteTime.now(),
+ });
+ }
+ }
+ if (handler.onOperationComplete) {
+ handler.onOperationComplete();
+ }
+ setRunning(false);
})
- }
- }
- if (handler.onOperationComplete) {
- handler.onOperationComplete()
- }
- setRunning(false)
- }).catch(error => {
- console.error(error)
+ .catch((error) => {
+ console.error(error);
- if (error instanceof TalerError) {
- handler.onNotification(buildUnifiedRequestErrorMessage(i18n, error))
- } else {
- const description = (error instanceof Error ?
- error.message : String(error)) as TranslatedString
+ if (error instanceof TalerError) {
+ handler.onNotification(
+ buildUnifiedRequestErrorMessage(i18n, error),
+ );
+ } else {
+ const description = (
+ error instanceof Error ? error.message : String(error)
+ ) as TranslatedString;
- handler.onNotification({
- title: i18n.str`Operation failed`,
- type: "error",
- description,
- when: AbsoluteTime.now(),
- })
- }
+ handler.onNotification({
+ title: i18n.str`Operation failed`,
+ type: "error",
+ description,
+ when: AbsoluteTime.now(),
+ });
+ }
- if (handler.onOperationComplete) {
- handler.onOperationComplete()
- }
- setRunning(false)
- })
- }} >
- {running ? <Wait /> : children}
- </button>
+ if (handler.onOperationComplete) {
+ handler.onOperationComplete();
+ }
+ setRunning(false);
+ });
+ }}
+ >
+ {running ? <Wait /> : children}
+ </button>
+ );
}
function Wait(): VNode {
- return <Fragment>
- <style>{`
+ return (
+ <Fragment>
+ <style>
+ {`
#l1 { width: 120px;
height: 20px;
-webkit-mask: radial-gradient(circle closest-side, currentColor 90%, #0000) left/20% 100%;
@@ -125,7 +160,8 @@ function Wait(): VNode {
@keyframes l17 {
100% {background-size:120% 100%}
`}
- </style>
- <div id="l1" />
- </Fragment>
+ </style>
+ <div id="l1" />
+ </Fragment>
+ );
}
diff --git a/packages/web-util/src/components/ErrorLoadingMerchant.tsx b/packages/web-util/src/components/ErrorLoadingMerchant.tsx
new file mode 100644
index 000000000..7089266b9
--- /dev/null
+++ b/packages/web-util/src/components/ErrorLoadingMerchant.tsx
@@ -0,0 +1,147 @@
+/*
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { TalerError, TalerErrorCode, assertUnreachable } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { Attention } from "./Attention.js";
+import { useTranslationContext } from "../index.browser.js";
+
+export function ErrorLoading({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode {
+ const { i18n } = useTranslationContext()
+ switch (error.errorDetail.code) {
+ //////////////////
+ // Every error that can be produce in a Http Request
+ //////////////////
+ case TalerErrorCode.GENERIC_TIMEOUT: {
+ if (error.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The request reached a timeout, check your connection.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The request was cancelled.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) {
+ const { requestMethod, requestUrl, timeoutMs } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The request reached a timeout, check your connection.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) {
+ const { requestMethod, requestUrl, throttleStats } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`A lot of request were made to the same server and this action was throttled`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) {
+ const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`The response of the request is malformed.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) {
+ const { requestMethod, requestUrl } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`Could not complete the request due to a network problem.`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) {
+ const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail
+ return <Attention type="danger" title={i18n.str`Unexpected request error`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+ assertUnreachable(1 as never)
+ }
+ //////////////////
+ // Every other error
+ //////////////////
+ // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ // return <Attention type="danger" title={i18n.str``}>
+ // </Attention>
+ // }
+ //////////////////
+ // Default message for unhandled case
+ //////////////////
+ default: return <Attention type="danger" title={i18n.str`Unexpected error`}>
+ {error.message}
+ {showDetail &&
+ <pre class="whitespace-break-spaces ">
+ {JSON.stringify(error.errorDetail, undefined, 2)}
+ </pre>
+ }
+ </Attention>
+ }
+}
+
diff --git a/packages/web-util/src/context/activity.ts b/packages/web-util/src/context/activity.ts
index 460c01096..d12d1efb6 100644
--- a/packages/web-util/src/context/activity.ts
+++ b/packages/web-util/src/context/activity.ts
@@ -14,11 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ObservabilityEvent, TalerAuthenticationHttpClient, TalerBankConversionHttpClient, TalerCoreBankHttpClient, TalerMerchantInstanceHttpClient, TalerMerchantManagementHttpClient } from "@gnu-taler/taler-util";
+import { ChallengerHttpClient, ObservabilityEvent, TalerAuthenticationHttpClient, TalerBankConversionHttpClient, TalerCoreBankHttpClient, TalerExchangeHttpClient, TalerMerchantInstanceHttpClient, TalerMerchantManagementHttpClient } from "@gnu-taler/taler-util";
type Listener<Event> = (e: Event) => void;
type Unsuscriber = () => void;
-export type Suscriber<Event> = (fn: Listener<Event>) => Unsuscriber;
+export type Subscriber<Event> = (fn: Listener<Event>) => Unsuscriber;
export class ActiviyTracker<Event> {
private observers = new Array<Listener<Event>>();
@@ -26,7 +26,7 @@ export class ActiviyTracker<Event> {
this.notify = this.notify.bind(this)
this.subscribe = this.subscribe.bind(this)
}
- notify(data: Event) {
+ notify(data: Event): void {
this.observers.forEach((observer) => observer(data))
}
subscribe(func: Listener<Event>): Unsuscriber {
@@ -50,15 +50,18 @@ export interface APIClient<T, C> {
getRemoteConfig(): Promise<C>;
VERSION: string;
lib: T,
- onActivity: Suscriber<ObservabilityEvent>;
+ onActivity: Subscriber<ObservabilityEvent>;
cancelRequest(id: string): void;
}
export interface MerchantLib {
- management: TalerMerchantManagementHttpClient;
+ instance: TalerMerchantManagementHttpClient;
authenticate: TalerAuthenticationHttpClient;
- instance: (instanceId: string) => TalerMerchantInstanceHttpClient;
- impersonate: (instanceId: string) => TalerAuthenticationHttpClient;
+ subInstanceApi: (instanceId: string) => MerchantLib;
+}
+
+export interface ExchangeLib {
+ exchange: TalerExchangeHttpClient;
}
export interface BankLib {
@@ -67,3 +70,7 @@ export interface BankLib {
auth: (user: string) => TalerAuthenticationHttpClient;
}
+export interface ChallengerLib {
+ challenger: ChallengerHttpClient;
+}
+
diff --git a/packages/web-util/src/context/api.ts b/packages/web-util/src/context/api.ts
index 7923532b6..c1eaa37f8 100644
--- a/packages/web-util/src/context/api.ts
+++ b/packages/web-util/src/context/api.ts
@@ -25,6 +25,9 @@ import { useContext } from "preact/hooks";
import { defaultRequestHandler } from "../utils/request.js";
interface Type {
+ /**
+ * @deprecated this show not be used
+ */
request: typeof defaultRequestHandler;
bankCore: TalerCoreBankHttpClient,
bankIntegration: TalerBankIntegrationHttpClient,
diff --git a/packages/web-util/src/context/bank-api.ts b/packages/web-util/src/context/bank-api.ts
index bd0653451..3f6a32f4b 100644
--- a/packages/web-util/src/context/bank-api.ts
+++ b/packages/web-util/src/context/bank-api.ts
@@ -25,7 +25,7 @@ import {
TalerCoreBankCacheEviction,
TalerCoreBankHttpClient,
TalerCorebankApi,
- TalerError
+ TalerError,
} from "@gnu-taler/taler-util";
import {
ComponentChildren,
@@ -35,7 +35,7 @@ import {
h,
} from "preact";
import { useContext, useEffect, useState } from "preact/hooks";
-import { APIClient, ActiviyTracker, BankLib, Suscriber } from "./activity.js";
+import { APIClient, ActiviyTracker, BankLib, Subscriber } from "./activity.js";
import { useTranslationContext } from "./translation.js";
import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
@@ -49,14 +49,15 @@ export type BankContextType = {
config: TalerCorebankApi.Config;
lib: BankLib;
hints: VersionHint[];
- onActivity: Suscriber<ObservabilityEvent>;
+ onActivity: Subscriber<ObservabilityEvent>;
cancelRequest: (eventId: string) => void;
};
// @ts-expect-error default value to undefined, should it be another thing?
const BankContext = createContext<BankContextType>(undefined);
-export const useBankCoreApiContext = (): BankContextType => useContext(BankContext);
+export const useBankCoreApiContext = (): BankContextType =>
+ useContext(BankContext);
enum VersionHint {
NONE,
@@ -65,7 +66,7 @@ enum VersionHint {
type Evictors = {
conversion?: CacheEvictor<TalerBankConversionCacheEviction>;
bank?: CacheEvictor<TalerCoreBankCacheEviction>;
-}
+};
type ConfigResult<T> =
| undefined
@@ -73,6 +74,8 @@ type ConfigResult<T> =
| { type: "incompatible"; result: T; supported: string }
| { type: "error"; error: TalerError };
+const CONFIG_FAIL_TRY_AGAIN_MS = 5000;
+
export const BankApiProvider = ({
baseUrl,
children,
@@ -81,17 +84,21 @@ export const BankApiProvider = ({
}: {
baseUrl: URL;
children: ComponentChildren;
- evictors?: Evictors,
+ evictors?: Evictors;
frameOnError: FunctionComponent<{ children: ComponentChildren }>;
}): VNode => {
- const [checked, setChecked] = useState<ConfigResult<TalerCorebankApi.Config>>();
+ const [checked, setChecked] =
+ useState<ConfigResult<TalerCorebankApi.Config>>();
const { i18n } = useTranslationContext();
- const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } = buildBankApiClient(baseUrl, evictors);
+ const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
+ buildBankApiClient(baseUrl, evictors);
useEffect(() => {
- getRemoteConfig()
- .then((config) => {
+ let keepRetrying = true;
+ async function testConfig(): Promise<void> {
+ try {
+ const config = await getRemoteConfig();
if (LibtoolVersion.compare(VERSION, config.version)) {
setChecked({ type: "ok", config, hints: [] });
} else {
@@ -101,16 +108,30 @@ export const BankApiProvider = ({
supported: VERSION,
});
}
- })
- .catch((error: unknown) => {
+ } catch (error) {
if (error instanceof TalerError) {
+ if (keepRetrying) {
+ setTimeout(() => {
+ testConfig();
+ }, CONFIG_FAIL_TRY_AGAIN_MS);
+ }
setChecked({ type: "error", error });
+ } else {
+ setChecked({ type: "error", error: TalerError.fromException(error) });
}
- });
+ }
+ }
+ testConfig();
+ return () => {
+ // on unload, stop retry
+ keepRetrying = false;
+ };
}, []);
if (checked === undefined) {
- return h(frameOnError, { children: h("div", {}, "checking compatibility with server...") });
+ return h(frameOnError, {
+ children: h("div", {}, "checking compatibility with server..."),
+ });
}
if (checked.type === "error") {
return h(frameOnError, {
@@ -141,7 +162,9 @@ export const BankApiProvider = ({
});
};
-function buildBankApiClient(url: URL, evictors: Evictors,
+function buildBankApiClient(
+ url: URL,
+ evictors: Evictors,
): APIClient<BankLib, TalerCorebankApi.Config> {
const httpFetch = new BrowserFetchHttpLib({
enableThrottling: true,
@@ -154,11 +177,7 @@ function buildBankApiClient(url: URL, evictors: Evictors,
},
});
- const bank = new TalerCoreBankHttpClient(
- url.href,
- httpLib,
- evictors.bank,
- );
+ const bank = new TalerCoreBankHttpClient(url.href, httpLib, evictors.bank);
const conversion = new TalerBankConversionHttpClient(
bank.getConversionInfoAPI().href,
httpLib,
@@ -170,32 +189,36 @@ function buildBankApiClient(url: URL, evictors: Evictors,
httpLib,
);
- async function getRemoteConfig() {
- const resp = await bank.getConfig()
- return resp.body
+ async function getRemoteConfig(): Promise<TalerCorebankApi.Config> {
+ const resp = await bank.getConfig();
+ if (resp.type === "fail") {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ }
+ return resp.body;
}
return {
getRemoteConfig,
VERSION: bank.PROTOCOL_VERSION,
lib: {
- bank, conversion, auth
+ bank,
+ conversion,
+ auth,
},
onActivity: tracker.subscribe,
cancelRequest: httpLib.cancelRequest,
};
}
-
export const BankApiProviderTesting = ({
children,
value,
}: {
- value: BankContextType
+ value: BankContextType;
children: ComponentChildren;
}): VNode => {
return h(BankContext.Provider, {
value,
children,
});
-}
+};
diff --git a/packages/web-util/src/context/challenger-api.ts b/packages/web-util/src/context/challenger-api.ts
new file mode 100644
index 000000000..8748f5f69
--- /dev/null
+++ b/packages/web-util/src/context/challenger-api.ts
@@ -0,0 +1,213 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CacheEvictor,
+ ChallengerApi,
+ ChallengerCacheEviction,
+ ChallengerHttpClient,
+ LibtoolVersion,
+ ObservabilityEvent,
+ ObservableHttpClientLibrary,
+ TalerError
+} from "@gnu-taler/taler-util";
+import {
+ ComponentChildren,
+ FunctionComponent,
+ VNode,
+ createContext,
+ h,
+} from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
+import {
+ APIClient,
+ ActiviyTracker,
+ ChallengerLib,
+ Subscriber
+} from "./activity.js";
+import { useTranslationContext } from "./translation.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type ChallengerContextType = {
+ url: URL;
+ config: ChallengerApi.ChallengerTermsOfServiceResponse;
+ lib: ChallengerLib;
+ hints: VersionHint[];
+ onActivity: Subscriber<ObservabilityEvent>;
+ cancelRequest: (eventId: string) => void;
+};
+
+// @ts-expect-error default value to undefined, should it be another thing?
+const ChallengerContext = createContext<ChallengerContextType>(undefined);
+
+export const useChallengerApiContext = (): ChallengerContextType =>
+ useContext(ChallengerContext);
+
+enum VersionHint {
+ NONE,
+}
+
+type Evictors = {
+ challenger?: CacheEvictor<ChallengerCacheEviction>;
+}
+
+type ConfigResult<T> =
+ | undefined
+ | { type: "ok"; config: T; hints: VersionHint[] }
+ | { type: "incompatible"; result: T; supported: string }
+ | { type: "error"; error: TalerError };
+
+const CONFIG_FAIL_TRY_AGAIN_MS = 5000;
+
+export const ChallengerApiProvider = ({
+ baseUrl,
+ children,
+ frameOnError,
+ evictors = {},
+}: {
+ baseUrl: URL;
+ children: ComponentChildren;
+ evictors?: Evictors;
+ frameOnError: FunctionComponent<{ children: ComponentChildren }>;
+}): VNode => {
+ const [checked, setChecked] =
+ useState<ConfigResult<ChallengerApi.ChallengerTermsOfServiceResponse>>();
+ const { i18n } = useTranslationContext();
+
+ const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
+ buildChallengerApiClient(baseUrl, evictors);
+
+ useEffect(() => {
+ let keepRetrying = true;
+ async function testConfig(): Promise<void> {
+ try {
+ const config = await getRemoteConfig();
+ if (LibtoolVersion.compare(VERSION, config.version)) {
+ setChecked({ type: "ok", config, hints: [] });
+ } else {
+ setChecked({
+ type: "incompatible",
+ result: config,
+ supported: VERSION,
+ });
+ }
+ } catch (error) {
+ if (error instanceof TalerError) {
+ if (keepRetrying) {
+ setTimeout(() => {
+ testConfig();
+ }, CONFIG_FAIL_TRY_AGAIN_MS);
+ }
+ setChecked({ type: "error", error });
+ } else {
+ setChecked({ type: "error", error: TalerError.fromException(error) });
+ }
+ }
+ }
+ testConfig();
+ return () => {
+ // on unload, stop retry
+ keepRetrying = false;
+ };
+ }, []);
+
+ if (checked === undefined) {
+ return h(frameOnError, {
+ children: h("div", {}, "checking compatibility with server..."),
+ });
+ }
+ if (checked.type === "error") {
+ return h(frameOnError, {
+ children: h(ErrorLoading, { error: checked.error, showDetail: true }),
+ });
+ }
+ if (checked.type === "incompatible") {
+ return h(frameOnError, {
+ children: h(
+ "div",
+ {},
+ i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.version}"`,
+ ),
+ });
+ }
+
+ const value: ChallengerContextType = {
+ url: baseUrl,
+ config: checked.config,
+ onActivity: onActivity,
+ lib,
+ cancelRequest,
+ hints: checked.hints,
+ };
+ return h(ChallengerContext.Provider, {
+ value,
+ children,
+ });
+};
+
+function buildChallengerApiClient(
+ url: URL,
+ evictors: Evictors,
+): APIClient<ChallengerLib, ChallengerApi.ChallengerTermsOfServiceResponse> {
+ const httpFetch = new BrowserFetchHttpLib({
+ enableThrottling: true,
+ requireTls: false,
+ });
+ const tracker = new ActiviyTracker<ObservabilityEvent>();
+ const httpLib = new ObservableHttpClientLibrary(httpFetch, {
+ observe(ev) {
+ tracker.notify(ev);
+ },
+ });
+
+ const challenger = new ChallengerHttpClient(url.href, httpLib, evictors.challenger);
+
+ async function getRemoteConfig(): Promise<ChallengerApi.ChallengerTermsOfServiceResponse> {
+ const resp = await challenger.getConfig();
+ if (resp.type === "fail") {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ }
+ return resp.body;
+ }
+
+ return {
+ getRemoteConfig,
+ VERSION: challenger.PROTOCOL_VERSION,
+ lib: {
+ challenger,
+ },
+ onActivity: tracker.subscribe,
+ cancelRequest: httpLib.cancelRequest,
+ };
+}
+
+export const ChallengerApiProviderTesting = ({
+ children,
+ value,
+}: {
+ value: ChallengerContextType;
+ children: ComponentChildren;
+}): VNode => {
+ return h(ChallengerContext.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/web-util/src/context/exchange-api.ts b/packages/web-util/src/context/exchange-api.ts
new file mode 100644
index 000000000..39f889ba9
--- /dev/null
+++ b/packages/web-util/src/context/exchange-api.ts
@@ -0,0 +1,217 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CacheEvictor,
+ LibtoolVersion,
+ ObservabilityEvent,
+ ObservableHttpClientLibrary,
+ TalerError,
+ TalerExchangeApi,
+ TalerExchangeCacheEviction,
+ TalerExchangeHttpClient
+} from "@gnu-taler/taler-util";
+import {
+ ComponentChildren,
+ FunctionComponent,
+ VNode,
+ createContext,
+ h,
+} from "preact";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { BrowserFetchHttpLib, ErrorLoading, useTranslationContext } from "../index.browser.js";
+import {
+ APIClient,
+ ActiviyTracker,
+ ExchangeLib,
+ Subscriber,
+} from "./activity.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type ExchangeContextType = {
+ url: URL;
+ config: TalerExchangeApi.ExchangeVersionResponse;
+ lib: ExchangeLib;
+ hints: VersionHint[];
+ onActivity: Subscriber<ObservabilityEvent>;
+ cancelRequest: (eventId: string) => void;
+};
+
+// FIXME: below
+// @ts-expect-error default value to undefined, should it be another thing?
+const ExchangeContext = createContext<ExchangeContextType>(undefined);
+
+export const useExchangeApiContext = (): ExchangeContextType =>
+ useContext(ExchangeContext);
+
+enum VersionHint {
+ NONE,
+}
+
+type Evictors = {
+ exchange?: CacheEvictor<TalerExchangeCacheEviction>;
+};
+
+type ConfigResult<T> =
+ | undefined
+ | { type: "ok"; config: T; hints: VersionHint[] }
+ | ConfigResultFail<T>;
+
+type ConfigResultFail<T> =
+ | { type: "incompatible"; result: T; supported: string }
+ | { type: "error"; error: TalerError };
+
+const CONFIG_FAIL_TRY_AGAIN_MS = 5000;
+
+export const ExchangeApiProvider = ({
+ baseUrl,
+ children,
+ evictors = {},
+ frameOnError,
+}: {
+ baseUrl: URL;
+ evictors?: Evictors;
+ children: ComponentChildren;
+ frameOnError: FunctionComponent<{ children: ComponentChildren }>;
+}): VNode => {
+ const [checked, setChecked] =
+ useState<ConfigResult<TalerExchangeApi.ExchangeVersionResponse>>();
+ const { i18n } = useTranslationContext();
+
+ const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
+ buildExchangeApiClient(baseUrl, evictors);
+
+ useEffect(() => {
+ let keepRetrying = true;
+ async function testConfig(): Promise<void> {
+ try {
+ const config = await getRemoteConfig();
+ if (LibtoolVersion.compare(VERSION, config.version)) {
+ setChecked({ type: "ok", config, hints: [] });
+ } else {
+ setChecked({
+ type: "incompatible",
+ result: config,
+ supported: VERSION,
+ });
+ }
+ } catch (error) {
+ if (error instanceof TalerError) {
+ if (keepRetrying) {
+ setTimeout(() => {
+ testConfig();
+ }, CONFIG_FAIL_TRY_AGAIN_MS);
+ }
+ setChecked({ type: "error", error });
+ } else {
+ setChecked({ type: "error", error: TalerError.fromException(error) });
+ }
+ }
+ }
+ testConfig();
+ return () => {
+ // on unload, stop retry
+ keepRetrying = false;
+ };
+ }, []);
+
+ if (checked === undefined) {
+ return h(frameOnError, {
+ children: h("div", {}, "checking compatibility with server..."),
+ });
+ }
+ if (checked.type === "error") {
+ return h(frameOnError, {
+ children: h(ErrorLoading, { error: checked.error, showDetail: true }),
+ });
+ }
+ if (checked.type === "incompatible") {
+ return h(frameOnError, {
+ children: h(
+ "div",
+ {},
+ i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.version}"`,
+ ),
+ });
+ }
+
+ const value: ExchangeContextType = {
+ url: baseUrl,
+ config: checked.config,
+ onActivity: onActivity,
+ lib,
+ cancelRequest,
+ hints: checked.hints,
+ };
+ return h(ExchangeContext.Provider, {
+ value,
+ children,
+ });
+};
+
+function buildExchangeApiClient(
+ url: URL,
+ evictors: Evictors,
+): APIClient<ExchangeLib, TalerExchangeApi.ExchangeVersionResponse> {
+ const httpFetch = new BrowserFetchHttpLib({
+ enableThrottling: true,
+ requireTls: false,
+ });
+ const tracker = new ActiviyTracker<ObservabilityEvent>();
+
+ const httpLib = new ObservableHttpClientLibrary(httpFetch, {
+ observe(ev) {
+ tracker.notify(ev);
+ },
+ });
+
+ const ex = new TalerExchangeHttpClient(url.href, httpLib, evictors.exchange);
+
+ async function getRemoteConfig(): Promise<TalerExchangeApi.ExchangeVersionResponse> {
+ const resp = await ex.getConfig();
+ if (resp.type === "fail") {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ }
+ return resp.body;
+ }
+
+ return {
+ getRemoteConfig,
+ VERSION: ex.PROTOCOL_VERSION,
+ lib: {
+ exchange: ex,
+ },
+ onActivity: tracker.subscribe,
+ cancelRequest: httpLib.cancelRequest,
+ };
+}
+
+export const ExchangeApiProviderTesting = ({
+ children,
+ value,
+}: {
+ value: ExchangeContextType;
+ children: ComponentChildren;
+}): VNode => {
+ return h(ExchangeContext.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/web-util/src/context/index.ts b/packages/web-util/src/context/index.ts
index 0e28b844a..7e30ecd09 100644
--- a/packages/web-util/src/context/index.ts
+++ b/packages/web-util/src/context/index.ts
@@ -5,6 +5,8 @@ export {
useTranslationContext
} from "./translation.js";
export * from "./bank-api.js";
+export * from "./challenger-api.js";
export * from "./merchant-api.js";
+export * from "./exchange-api.js";
export * from "./navigation.js";
export * from "./wallet-integration.js";
diff --git a/packages/web-util/src/context/merchant-api.ts b/packages/web-util/src/context/merchant-api.ts
index 23ea05cd2..03c95d48e 100644
--- a/packages/web-util/src/context/merchant-api.ts
+++ b/packages/web-util/src/context/merchant-api.ts
@@ -23,7 +23,6 @@ import {
TalerError,
TalerMerchantApi,
TalerMerchantInstanceCacheEviction,
- TalerMerchantInstanceHttpClient,
TalerMerchantManagementCacheEviction,
TalerMerchantManagementHttpClient,
} from "@gnu-taler/taler-util";
@@ -35,14 +34,13 @@ import {
h,
} from "preact";
import { useContext, useEffect, useState } from "preact/hooks";
+import { BrowserFetchHttpLib } from "../index.browser.js";
import {
APIClient,
ActiviyTracker,
MerchantLib,
- Suscriber,
+ Subscriber,
} from "./activity.js";
-import { useTranslationContext } from "./translation.js";
-import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
/**
*
@@ -54,8 +52,9 @@ export type MerchantContextType = {
config: TalerMerchantApi.VersionResponse;
lib: MerchantLib;
hints: VersionHint[];
- onActivity: Suscriber<ObservabilityEvent>;
+ onActivity: Subscriber<ObservabilityEvent>;
cancelRequest: (eventId: string) => void;
+ changeBackend: (url: URL) => void;
};
// FIXME: below
@@ -70,10 +69,9 @@ enum VersionHint {
}
type Evictors = {
- management?: CacheEvictor<TalerMerchantManagementCacheEviction>;
- instance?: (
- instanceId: string,
- ) => CacheEvictor<TalerMerchantInstanceCacheEviction>;
+ management?: CacheEvictor<
+ TalerMerchantManagementCacheEviction | TalerMerchantInstanceCacheEviction
+ >;
};
type ConfigResult<T> =
@@ -85,6 +83,8 @@ export type ConfigResultFail<T> =
| { type: "incompatible"; result: T; supported: string }
| { type: "error"; error: TalerError };
+const CONFIG_FAIL_TRY_AGAIN_MS = 5000;
+
export const MerchantApiProvider = ({
baseUrl,
children,
@@ -94,18 +94,23 @@ export const MerchantApiProvider = ({
baseUrl: URL;
evictors?: Evictors;
children: ComponentChildren;
- frameOnError: FunctionComponent<{ state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined }>;
+ frameOnError: FunctionComponent<{
+ state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined;
+ }>;
}): VNode => {
const [checked, setChecked] =
useState<ConfigResult<TalerMerchantApi.VersionResponse>>();
- const { i18n } = useTranslationContext();
+
+ const [merchantEndpoint, changeMerchantEndpoint] = useState(baseUrl);
const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
- buildMerchantApiClient(baseUrl, evictors);
+ buildMerchantApiClient(merchantEndpoint, evictors);
useEffect(() => {
- getRemoteConfig()
- .then((config) => {
+ let keepRetrying = true;
+ async function testConfig(): Promise<void> {
+ try {
+ const config = await getRemoteConfig();
if (LibtoolVersion.compare(VERSION, config.version)) {
setChecked({ type: "ok", config, hints: [] });
} else {
@@ -115,12 +120,24 @@ export const MerchantApiProvider = ({
supported: VERSION,
});
}
- })
- .catch((error: unknown) => {
+ } catch (error) {
if (error instanceof TalerError) {
+ if (keepRetrying) {
+ setTimeout(() => {
+ testConfig();
+ }, CONFIG_FAIL_TRY_AGAIN_MS);
+ }
setChecked({ type: "error", error });
+ } else {
+ setChecked({ type: "error", error: TalerError.fromException(error) });
}
- });
+ }
+ }
+ testConfig();
+ return () => {
+ // on unload, stop retry
+ keepRetrying = false;
+ };
}, []);
if (!checked || checked.type !== "ok") {
@@ -128,11 +145,12 @@ export const MerchantApiProvider = ({
}
const value: MerchantContextType = {
- url: baseUrl,
+ url: merchantEndpoint,
config: checked.config,
onActivity: onActivity,
lib,
cancelRequest,
+ changeBackend: changeMerchantEndpoint,
hints: checked.hints,
};
return h(MerchantContext.Provider, {
@@ -157,40 +175,39 @@ function buildMerchantApiClient(
},
});
- const management = new TalerMerchantManagementHttpClient(
+ const instance = new TalerMerchantManagementHttpClient(
url.href,
httpLib,
evictors.management,
);
- const instance = (instanceId: string) =>
- new TalerMerchantInstanceHttpClient(
- management.getSubInstanceAPI(instanceId).href,
- httpLib,
- evictors.instance ? evictors.instance(instanceId) : undefined,
- );
const authenticate = new TalerAuthenticationHttpClient(
- management.getAuthenticationAPI().href,
+ instance.getAuthenticationAPI().href,
httpLib,
);
- const impersonate = (instanceId: string) =>
- new TalerAuthenticationHttpClient(
- instance(instanceId).getAuthenticationAPI().href,
- httpLib,
+
+ function getSubInstanceAPI(instanceId: string): MerchantLib {
+ const api = buildMerchantApiClient(
+ instance.getSubInstanceAPI(instanceId) as URL,
+ evictors,
);
+ return api.lib;
+ }
async function getRemoteConfig(): Promise<TalerMerchantApi.VersionResponse> {
- const resp = await management.getConfig();
+ const resp = await instance.getConfig();
+ if (resp.type === "fail") {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ }
return resp.body;
}
return {
getRemoteConfig,
- VERSION: management.PROTOCOL_VERSION,
+ VERSION: instance.PROTOCOL_VERSION,
lib: {
- management,
- authenticate,
- impersonate,
instance,
+ authenticate,
+ subInstanceApi: getSubInstanceAPI,
},
onActivity: tracker.subscribe,
cancelRequest: httpLib.cancelRequest,
diff --git a/packages/web-util/src/context/navigation.ts b/packages/web-util/src/context/navigation.ts
index a2fe3ff12..c2f2bbbc1 100644
--- a/packages/web-util/src/context/navigation.ts
+++ b/packages/web-util/src/context/navigation.ts
@@ -16,17 +16,13 @@
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext, useEffect, useState } from "preact/hooks";
-import { AppLocation, ObjectOf, Location, findMatch, RouteDefinition } from "../utils/route.js";
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(
- pagesMap: T,
-): Location<T> | undefined {
- const pageList = Object.keys(pagesMap as object) as Array<keyof T>;
- const { path, params } = useNavigationContext();
-
- return findMatch(pagesMap, pageList, path, params);
-}
+import {
+ AppLocation,
+ ObjectOf,
+ Location,
+ findMatch,
+ RouteDefinition,
+} from "../utils/route.js";
/**
*
@@ -35,7 +31,7 @@ export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(
export type Type = {
path: string;
- params: Record<string, string>;
+ params: Record<string, string[]>;
navigateTo: (path: AppLocation) => void;
// addNavigationListener: (listener: (path: string, params: Record<string, string>) => void) => (() => void);
};
@@ -45,13 +41,29 @@ const Context = createContext<Type>(undefined);
export const useNavigationContext = (): Type => useContext(Context);
-function getPathAndParamsFromWindow() {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(
+ pagesMap: T,
+): Location<T> | undefined {
+ const pageList = Object.keys(pagesMap as object) as Array<keyof T>;
+ const { path, params } = useNavigationContext();
+
+ return findMatch(pagesMap, pageList, path, params);
+}
+
+function getPathAndParamsFromWindow(): {
+ path: string;
+ params: Record<string, string[]>;
+} {
const path =
typeof window !== "undefined" ? window.location.hash.substring(1) : "/";
- const params: Record<string, string> = {};
+ const params: Record<string, string[]> = {};
if (typeof window !== "undefined") {
for (const [key, value] of new URLSearchParams(window.location.search)) {
- params[key] = value;
+ if (!params[key]) {
+ params[key] = [];
+ }
+ params[key].push(value);
}
}
return { path, params };
@@ -80,14 +92,14 @@ export const BrowserHashNavigationProvider = ({
"Can't use BrowserHashNavigationProvider if there is no window object",
);
}
- function navigateTo(path: string) {
+ function navigateTo(path: string): void {
const { params } = getPathAndParamsFromWindow();
setState({ path, params });
window.location.href = path;
}
useEffect(() => {
- function eventListener() {
+ function eventListener(): void {
setState(getPathAndParamsFromWindow());
}
window.addEventListener(PopStateEventType, eventListener);
diff --git a/packages/web-util/src/forms/Calendar.tsx b/packages/web-util/src/forms/Calendar.tsx
index a0df639f3..b08129f56 100644
--- a/packages/web-util/src/forms/Calendar.tsx
+++ b/packages/web-util/src/forms/Calendar.tsx
@@ -1,20 +1,40 @@
-import { AbsoluteTime } from "@gnu-taler/taler-util"
-import { add as dateAdd, sub as dateSub, eachDayOfInterval, endOfMonth, endOfWeek, format, getMonth, getYear, isSameDay, isSameMonth, startOfDay, startOfMonth, startOfWeek } from "date-fns"
-import { VNode, h } from "preact"
-import { useState } from "preact/hooks"
-import { useTranslationContext } from "../index.browser.js"
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import {
+ add as dateAdd,
+ sub as dateSub,
+ eachDayOfInterval,
+ endOfMonth,
+ endOfWeek,
+ format,
+ getMonth,
+ getYear,
+ isSameDay,
+ isSameMonth,
+ startOfDay,
+ startOfMonth,
+ startOfWeek,
+} from "date-fns";
+import { VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useTranslationContext } from "../index.browser.js";
-export function Calendar({ value, onChange }: { value: AbsoluteTime | undefined, onChange: (v: AbsoluteTime) => void }): VNode {
- const today = startOfDay(new Date())
- const selected = !value ? today : new Date(AbsoluteTime.toStampMs(value))
- const [showingDate, setShowingDate] = useState(selected)
- const month = getMonth(showingDate)
- const year = getYear(showingDate)
+export function Calendar({
+ value,
+ onChange,
+}: {
+ value: AbsoluteTime | undefined;
+ onChange: (v: AbsoluteTime) => void;
+}): VNode {
+ const today = startOfDay(new Date());
+ const selected = !value ? today : new Date(AbsoluteTime.toStampMs(value));
+ const [showingDate, setShowingDate] = useState(selected);
+ const month = getMonth(showingDate);
+ const year = getYear(showingDate);
const start = startOfWeek(startOfMonth(showingDate));
const end = endOfWeek(endOfMonth(showingDate));
const daysInMonth = eachDayOfInterval({ start, end });
- const { i18n } = useTranslationContext()
+ const { i18n } = useTranslationContext();
const monthNames = [
i18n.str`January`,
i18n.str`February`,
@@ -28,92 +48,139 @@ export function Calendar({ value, onChange }: { value: AbsoluteTime | undefined,
i18n.str`October`,
i18n.str`November`,
i18n.str`December`,
- ]
- return <div class="text-center p-2">
- <div class="flex items-center text-gray-900">
- <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
- onClick={() => {
- setShowingDate(dateSub(showingDate, { years: 1 }))
- }}>
- <span class="sr-only">
- {i18n.str`Previous year`}
- </span>
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
- </svg>
- </button>
- <div class="flex-auto text-sm font-semibold">{year}</div>
- <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
- onClick={() => {
- setShowingDate(dateAdd(showingDate, { years: 1 }))
- }}>
- <span class="sr-only">
- {i18n.str`Next year`}
- </span>
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
- </svg>
- </button>
- </div>
- <div class="mt-4 flex items-center text-gray-900">
- <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
- onClick={() => {
- setShowingDate(dateSub(showingDate, { months: 1 }))
- }}>
- <span class="sr-only">
- {i18n.str`Previous month`}
- </span>
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
- </svg>
- </button>
- <div class="flex-auto text-sm font-semibold">{monthNames[month]}</div>
- <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 rounded-sm "
- onClick={() => {
- setShowingDate(dateAdd(showingDate, { months: 1 }))
- }}>
- <span class="sr-only">
- {i18n.str`Next month`}
- </span>
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
- </svg>
- </button>
- </div>
- <div class="mt-6 grid grid-cols-7 text-xs leading-6 text-gray-500">
- <div>M</div>
- <div>T</div>
- <div>W</div>
- <div>T</div>
- <div>F</div>
- <div>S</div>
- <div>S</div>
- </div>
- <div class="isolate mt-2">
- <div class="grid grid-cols-7 gap-px rounded-lg bg-gray-200 text-sm shadow ring-1 ring-gray-200">
- {daysInMonth.map(current => (
- <button type="button"
- data-month={isSameMonth(current, showingDate)}
- data-today={isSameDay(current, today)}
- data-selected={isSameDay(current, selected)}
- onClick={() => {
- onChange(AbsoluteTime.fromStampMs(current.getTime()))
- }}
- class="text-gray-400 hover:bg-gray-700 focus:z-10 py-1.5
+ ];
+ return (
+ <div class="text-center p-2">
+ <div class="flex items-center text-gray-900">
+ <button
+ type="button"
+ class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+ onClick={() => {
+ setShowingDate(dateSub(showingDate, { years: 1 }));
+ }}
+ >
+ <span class="sr-only">{i18n.str`Previous year`}</span>
+ <svg
+ class="h-5 w-5"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </button>
+ <div class="flex-auto text-sm font-semibold">{year}</div>
+ <button
+ type="button"
+ class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+ onClick={() => {
+ setShowingDate(dateAdd(showingDate, { years: 1 }));
+ }}
+ >
+ <span class="sr-only">{i18n.str`Next year`}</span>
+ <svg
+ class="h-5 w-5"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </button>
+ </div>
+ <div class="mt-4 flex items-center text-gray-900">
+ <button
+ type="button"
+ class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm"
+ onClick={() => {
+ setShowingDate(dateSub(showingDate, { months: 1 }));
+ }}
+ >
+ <span class="sr-only">{i18n.str`Previous month`}</span>
+ <svg
+ class="h-5 w-5"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </button>
+ <div class="flex-auto text-sm font-semibold">{monthNames[month]}</div>
+ <button
+ type="button"
+ class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 rounded-sm "
+ onClick={() => {
+ setShowingDate(dateAdd(showingDate, { months: 1 }));
+ }}
+ >
+ <span class="sr-only">{i18n.str`Next month`}</span>
+ <svg
+ class="h-5 w-5"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </button>
+ </div>
+ <div class="mt-6 grid grid-cols-7 text-xs leading-6 text-gray-500">
+ <div>M</div>
+ <div>T</div>
+ <div>W</div>
+ <div>T</div>
+ <div>F</div>
+ <div>S</div>
+ <div>S</div>
+ </div>
+ <div class="isolate mt-2">
+ <div class="grid grid-cols-7 gap-px rounded-lg bg-gray-200 text-sm shadow ring-1 ring-gray-200">
+ {daysInMonth.map((current, idx) => (
+ <button
+ type="button"
+ key={idx}
+ data-month={isSameMonth(current, showingDate)}
+ data-today={isSameDay(current, today)}
+ data-selected={isSameDay(current, selected)}
+ onClick={() => {
+ onChange(AbsoluteTime.fromStampMs(current.getTime()));
+ }}
+ class="text-gray-400 hover:bg-gray-700 focus:z-10 py-1.5
data-[month=false]:bg-gray-100 data-[month=true]:bg-white
data-[today=true]:font-semibold
data-[month=true]:text-gray-900
data-[today=true]:bg-red-300 data-[today=true]:hover:bg-red-200
data-[month=true]:hover:bg-gray-200
- data-[selected=true]:!bg-blue-400 data-[selected=true]:hover:!bg-blue-300 ">
- <time dateTime={format(current, "yyyy-MM-dd")}
- class="mx-auto flex h-7 w-7 py-4 px-5 sm:px-8 items-center justify-center rounded-full">
- {format(current, "dd")}
- </time>
- </button>
- ))}
+ data-[selected=true]:!bg-blue-400 data-[selected=true]:hover:!bg-blue-300 "
+ >
+ <time
+ dateTime={format(current, "yyyy-MM-dd")}
+ class="mx-auto flex h-7 w-7 py-4 px-5 sm:px-8 items-center justify-center rounded-full"
+ >
+ {format(current, "dd")}
+ </time>
+ </button>
+ ))}
+ </div>
+ {daysInMonth.length < 40 ? <div class="w-7 h-7 m-1.5" /> : undefined}
</div>
- {daysInMonth.length < 40 ? <div class="w-7 h-7 m-1.5" /> : undefined}
</div>
- </div>
+ );
}
diff --git a/packages/web-util/src/forms/Caption.tsx b/packages/web-util/src/forms/Caption.tsx
index 8facddec3..be4725ffa 100644
--- a/packages/web-util/src/forms/Caption.tsx
+++ b/packages/web-util/src/forms/Caption.tsx
@@ -1,27 +1,22 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
-import {
- LabelWithTooltipMaybeRequired
-} from "./InputLine.js";
+import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js";
+import { Addon } from "./FormProvider.js";
interface Props {
label: TranslatedString;
tooltip?: TranslatedString;
help?: TranslatedString;
- before?: VNode;
- after?: VNode;
+ before?: Addon;
+ after?: Addon;
}
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>
- )}
+ {before !== undefined && <RenderAddon addon={before} />}
<LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} />
- {after !== undefined && (
- <span class="pointer-events-none flex items-center pl-2">{after}</span>
- )}
+ {after !== undefined && <RenderAddon addon={after} />}
{help && (
<p class="mt-2 text-sm text-gray-500" id="email-description">
{help}
diff --git a/packages/web-util/src/forms/DefaultForm.tsx b/packages/web-util/src/forms/DefaultForm.tsx
index 1155401f5..338460170 100644
--- a/packages/web-util/src/forms/DefaultForm.tsx
+++ b/packages/web-util/src/forms/DefaultForm.tsx
@@ -1,15 +1,16 @@
-import { Fragment, h } from "preact";
+import { Fragment, VNode, h } from "preact";
import { FormProvider, FormProviderProps, FormState } from "./FormProvider.js";
import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
import { TranslatedString } from "@gnu-taler/taler-util";
+// import { FlexibleForm } from "./ui-form.js";
/**
* Flexible form uses a DoubleColumForm for design
* and may have a dynamic properties defined by
* behavior function.
*/
-export interface FlexibleForm<T extends object> {
- design: DoubleColumnForm;
+export interface FlexibleForm_Deprecated<T extends object> {
+ design: DoubleColumnForm_Deprecated;
behavior?: (form: Partial<T>) => FormState<T>;
}
@@ -20,9 +21,9 @@ export interface FlexibleForm<T extends object> {
* have a description.
* Every sections contain a set of fields.
*/
-export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>;
+export type DoubleColumnForm_Deprecated = Array<DoubleColumnFormSection_Deprecated | undefined>;
-export type DoubleColumnFormSection = {
+export type DoubleColumnFormSection_Deprecated = {
title: TranslatedString;
description?: TranslatedString;
fields: UIFormField[];
@@ -39,20 +40,20 @@ export function DefaultForm<T extends object>({
onSubmit,
children,
readOnly,
-}: Omit<FormProviderProps<T>, "computeFormState"> & { form: FlexibleForm<T> }) {
+}: Omit<FormProviderProps<T>, "computeFormState"> & { form: FlexibleForm_Deprecated<T> }): VNode {
return (
<FormProvider
initial={initial}
onUpdate={onUpdate}
onSubmit={onSubmit}
readOnly={readOnly}
- computeFormState={form.behavior}
+ // 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 key={i} class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
{section.title}
diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx
index f4616525b..5e08efb32 100644
--- a/packages/web-util/src/forms/FormProvider.tsx
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -4,10 +4,7 @@ import {
TranslatedString,
} from "@gnu-taler/taler-util";
import { ComponentChildren, VNode, createContext, h } from "preact";
-import {
- MutableRef,
- useState
-} from "preact/hooks";
+import { MutableRef, useState } from "preact/hooks";
export interface FormType<T extends object> {
value: MutableRef<Partial<T>>;
@@ -17,8 +14,7 @@ export interface FormType<T extends object> {
computeFormState?: (v: Partial<T>) => FormState<T>;
}
-//@ts-ignore
-export const FormContext = createContext<FormType<any>>({});
+export const FormContext = createContext<FormType<any>| undefined>(undefined);
/**
* Map of {[field]:FieldUIOptions}
@@ -26,29 +22,27 @@ export const FormContext = createContext<FormType<any>>({});
* - any native (string, number, etc...)
* - absoluteTime
* - amountJson
- *
- * except for:
+ *
+ * except for:
* - object => recurse into
* - array => behavior result and element field
*/
export type FormState<T extends object | undefined> = {
[field in keyof T]?: T[field] extends AbsoluteTime
- ? FieldUIOptions
- : T[field] extends AmountJson
- ? FieldUIOptions
- : T[field] extends Array<infer P extends object>
- ? InputArrayFieldState<P>
- : T[field] extends (object | undefined)
- ? FormState<T[field]>
- : FieldUIOptions;
+ ? FieldUIOptions
+ : T[field] extends AmountJson
+ ? FieldUIOptions
+ : T[field] extends Array<infer P extends object>
+ ? InputArrayFieldState<P>
+ : T[field] extends object | undefined
+ ? FormState<T[field]>
+ : FieldUIOptions;
};
/**
* Properties that can be defined by design or by computing state
*/
export type FieldUIOptions = {
- /* text to be shown next to the field */
- error?: TranslatedString;
/* instruction to be shown in the field */
placeholder?: TranslatedString;
/* long text help to be shown on demand */
@@ -63,13 +57,13 @@ export type FieldUIOptions = {
/* show a mark as required*/
required?: boolean;
-}
+};
/**
* properties only to be defined on design time
*/
-export interface UIFormProps<T extends object, K extends keyof T> extends FieldUIOptions {
-
+export interface UIFormProps<T extends object, K extends keyof T>
+ extends FieldUIOptions {
// property name of the object
name: K;
@@ -80,8 +74,17 @@ export interface UIFormProps<T extends object, K extends keyof T> extends FieldU
// converter to string and back
converter?: StringConverter<T[K]>;
+
+ handler?: UIFieldHandler;
}
+export type UIFieldHandler = {
+ value: string | undefined;
+ onChange: (s: string) => void;
+ state: FieldUIOptions;
+ error?: TranslatedString;
+};
+
export interface IconAddon {
type: "icon";
icon: VNode;
@@ -109,7 +112,7 @@ export interface InputArrayFieldState<P extends object> extends FieldUIOptions {
export type FormProviderProps<T extends object> = Omit<FormType<T>, "value"> & {
onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void;
children?: ComponentChildren;
-}
+};
export function FormProvider<T extends object>({
children,
@@ -119,7 +122,6 @@ export function FormProvider<T extends object>({
computeFormState,
readOnly,
}: FormProviderProps<T>): VNode {
-
const [state, setState] = useState<Partial<T>>(initial ?? {});
const value = { current: state };
const onUpdate = (v: typeof state) => {
diff --git a/packages/web-util/src/forms/Group.tsx b/packages/web-util/src/forms/Group.tsx
index 0645f6d97..f63fa4a9b 100644
--- a/packages/web-util/src/forms/Group.tsx
+++ b/packages/web-util/src/forms/Group.tsx
@@ -1,40 +1,43 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js";
+import { RenderAllFieldsByUiConfig, UIFormField, convertUiField } from "./forms.js";
+import { Addon, FormProvider } from "./FormProvider.js";
+import { useField } from "./useField.js";
+import { useTranslationContext } from "../index.browser.js";
+import { getConverterById } from "./converter.js";
interface Props {
- before?: TranslatedString;
- after?: TranslatedString;
- tooltipBefore?: TranslatedString;
- tooltipAfter?: TranslatedString;
+ label: TranslatedString;
+ tooltip?: TranslatedString;
+ help?: TranslatedString;
+ before?: Addon;
+ after?: Addon;
fields: UIFormField[];
}
export function Group({
before,
after,
- tooltipAfter,
- tooltipBefore,
+ label,
+ tooltip,
+ help,
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>
+ {before !== undefined && <RenderAddon addon={before} />}
+ <LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} />
+ {after !== undefined && <RenderAddon addon={after} />}
+ {help && (
+ <p class="mt-2 text-sm text-gray-500" id="email-description">
+ {help}
+ </p>
+ )}
<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} />
- )}
+ <RenderAllFieldsByUiConfig
+ fields={fields}
+ />
</div>
</div>
);
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
index 6245cf27c..6b792bfee 100644
--- a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
+++ b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
@@ -22,7 +22,7 @@
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,12 +43,12 @@ const initial: TargetObject = {
today: AbsoluteTime.now()
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
- type: "absoluteTime",
- props: {
+ type: "absoluteTimeText",
+ properties: {
label: "label of the field" as TranslatedString,
name: "today",
pattern: "dd/MM/yyyy HH:mm"
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.tsx b/packages/web-util/src/forms/InputAbsoluteTime.tsx
index ee18e5592..f5fd4fc50 100644
--- a/packages/web-util/src/forms/InputAbsoluteTime.tsx
+++ b/packages/web-util/src/forms/InputAbsoluteTime.tsx
@@ -1,35 +1,50 @@
import { AbsoluteTime } from "@gnu-taler/taler-util";
-import { InputLine } from "./InputLine.js";
-import { Fragment, VNode, h } from "preact";
import { format, parse } from "date-fns";
-import { Dialog } from "./Dialog.js";
-import { Calendar } from "./Calendar.js";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { useField } from "./useField.js";
+import { Calendar } from "./Calendar.js";
+import { Dialog } from "./Dialog.js";
import { UIFormProps } from "./FormProvider.js";
-import { TimePicker } from "./TimePicker.js";
+import { InputLine } from "./InputLine.js";
+import { useField } from "./useField.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
export function InputAbsoluteTime<T extends object, K extends keyof T>(
- props: { pattern?: string } & UIFormProps<T, K>,
+ properties: { pattern?: string } & UIFormProps<T, K>,
): VNode {
- const pattern = props.pattern ?? "dd/MM/yyyy";
- const [open, setOpen] = useState(false)
- const { value, onChange } = useField<T, K>(props.name);
+ const pattern = properties.pattern ?? "dd/MM/yyyy";
+ const [open, setOpen] = useState(false);
+
+ //FIXME: remove deprecated
+ const fieldCtx = useField<T, K>(properties.name);
+ const { value, onChange } =
+ properties.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(properties.name);
return (
<Fragment>
-
<InputLine<T, K>
type="text"
after={{
type: "button",
onClick: () => {
- setOpen(true)
+ setOpen(true);
},
// icon: <CalendarIcon class="h-6 w-6" />,
children: (
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
- </svg>)
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
+ />
+ </svg>
+ ),
}}
converter={{
//@ts-ignore
@@ -51,17 +66,19 @@ export function InputAbsoluteTime<T extends object, K extends keyof T>(
: format(v.t_ms, pattern);
},
}}
- {...props}
+ {...properties}
/>
- {open &&
+ {open && (
<Dialog onClose={() => setOpen(false)}>
- <Calendar value={value as AbsoluteTime ?? AbsoluteTime.now()}
+ <Calendar
+ value={(value as AbsoluteTime) ?? AbsoluteTime.now()}
onChange={(v) => {
- onChange(v as any)
- setOpen(false)
- }} />
+ onChange(v as any);
+ setOpen(false);
+ }}
+ />
</Dialog>
- }
+ )}
{/* {open &&
<Dialog onClose={() => setOpen(false)} >
<TimePicker value={value as AbsoluteTime ?? AbsoluteTime.now()}
diff --git a/packages/web-util/src/forms/InputAmount.stories.tsx b/packages/web-util/src/forms/InputAmount.stories.tsx
index c9f12a437..f05887515 100644
--- a/packages/web-util/src/forms/InputAmount.stories.tsx
+++ b/packages/web-util/src/forms/InputAmount.stories.tsx
@@ -22,7 +22,7 @@
import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,12 +43,12 @@ const initial: TargetObject = {
amount: Amounts.parseOrThrow("USD:10")
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "amount",
- props: {
+ properties: {
label: "label of the field" as TranslatedString,
name: "amount",
},
diff --git a/packages/web-util/src/forms/InputAmount.tsx b/packages/web-util/src/forms/InputAmount.tsx
index 7a8c08f76..647d2c823 100644
--- a/packages/web-util/src/forms/InputAmount.tsx
+++ b/packages/web-util/src/forms/InputAmount.tsx
@@ -3,34 +3,41 @@ import { VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
import { InputLine } from "./InputLine.js";
import { useField } from "./useField.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.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);
+ //FIXME: remove deprecated
+ const fieldCtx = useField<T, K>(props.name);
+ const { value } =
+ props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);
const currency =
!value || !(value as any).currency
? props.currency
: (value as any).currency;
return (
<InputLine<T, K>
+ {...props}
type="text"
before={{
type: "text",
text: currency as TranslatedString,
}}
- converter={{
- //@ts-ignore
- fromStringUI: (v): AmountJson => {
-
- return Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency);
- },
- //@ts-ignore
- toStringUI: (v: AmountJson) => {
- return v === undefined ? "" : Amounts.stringifyValue(v);
- },
- }}
- {...props}
+ //@ts-ignore
+ converter={
+ props.converter ?? {
+ fromStringUI: (v): AmountJson => {
+ return (
+ Amounts.parse(`${currency}:${v}`) ??
+ Amounts.zeroOfCurrency(currency)
+ );
+ },
+ toStringUI: (v: AmountJson) => {
+ return v === undefined ? "" : Amounts.stringifyValue(v);
+ },
+ }
+ }
/>
);
}
diff --git a/packages/web-util/src/forms/InputArray.stories.tsx b/packages/web-util/src/forms/InputArray.stories.tsx
index 8dbd3ff07..143e73f02 100644
--- a/packages/web-util/src/forms/InputArray.stories.tsx
+++ b/packages/web-util/src/forms/InputArray.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -49,23 +49,23 @@ const initial: TargetObject = {
}]
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "array",
- props: {
+ properties: {
label: "People" as TranslatedString,
name: "comment",
fields: [{
type: "text",
- props: {
+ properties: {
label: "the name" as TranslatedString,
name: "name",
}
}, {
type: "integer",
- props: {
+ properties: {
label: "the age" as TranslatedString,
name: "age",
}
diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx
index 7d9a1b378..1ac96437c 100644
--- a/packages/web-util/src/forms/InputArray.tsx
+++ b/packages/web-util/src/forms/InputArray.tsx
@@ -71,6 +71,14 @@ function Option({
);
}
+export function noHandlerPropsAndNoContextForField(
+ field: string | number | symbol,
+): never {
+ throw Error(
+ `Field ${field.toString()} doesn't have handler and is not in a form provider context.`,
+ );
+}
+
export function InputArray<T extends object, K extends keyof T>(
props: {
fields: UIFormField[];
@@ -78,7 +86,15 @@ export function InputArray<T extends object, K extends keyof T>(
} & UIFormProps<T, K>,
): VNode {
const { fields, labelField, name, label, required, tooltip } = props;
- const { value, onChange, state } = useField<T, K>(name);
+ // const { value, onChange, state } = useField<T, K>(name);
+ //FIXME: remove deprecated
+ const fieldCtx = useField<T, K>(props.name);
+ if (!props.handler && !fieldCtx) {
+ throw Error("");
+ }
+ const { value, onChange, state } =
+ props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);
+
const list = (value ?? []) as Array<Record<string, string | undefined>>;
const [selectedIndex, setSelected] = useState<number | undefined>(undefined);
const selected =
@@ -97,6 +113,7 @@ export function InputArray<T extends object, K extends keyof T>(
return (
<Option
label={v[labelField] as TranslatedString}
+ key={idx}
isSelected={selectedIndex === idx}
isLast={idx === list.length - 1}
disabled={selectedIndex !== undefined && selectedIndex !== idx}
@@ -107,7 +124,7 @@ export function InputArray<T extends object, K extends keyof T>(
/>
);
})}
- {!state.disabled &&
+ {!state.disabled && (
<div class="pt-2">
<Option
label={"Add..." as TranslatedString}
@@ -124,7 +141,7 @@ export function InputArray<T extends object, K extends keyof T>(
}}
/>
</div>
- }
+ )}
</div>
{selectedIndex !== undefined && (
/**
@@ -140,18 +157,19 @@ export function InputArray<T extends object, K extends keyof T>(
// elements should be present in the state object since this is expected to be an array
//@ts-ignore
- return state.elements[selectedIndex];
+ // return state.elements[selectedIndex];
+ return {}
}}
onSubmit={(v) => {
const newValue = [...list];
newValue.splice(selectedIndex, 1, v);
- onChange(newValue as T[K]);
+ onChange(newValue as any);
setSelected(undefined);
}}
onUpdate={(v) => {
const newValue = [...list];
newValue.splice(selectedIndex, 1, v);
- onChange(newValue as T[K]);
+ onChange(newValue as any);
}}
>
<div class="px-4 py-6">
@@ -170,7 +188,7 @@ export function InputArray<T extends object, K extends keyof T>(
onClick={() => {
const newValue = [...list];
newValue.splice(selectedIndex, 1);
- onChange(newValue as T[K]);
+ onChange(newValue as any);
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 "
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
index b950d3d02..786dfe5bc 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,12 +43,12 @@ const initial: TargetObject = {
comment: "0"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "choiceHorizontal",
- props: {
+ properties: {
label: "label of the field" as TranslatedString,
name: "comment",
choices: [{
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
index 778b73c75..d8361718d 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
@@ -3,6 +3,7 @@ import { Fragment, VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
import { useField } from "./useField.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
export interface ChoiceH<V> {
label: TranslatedString;
@@ -11,22 +12,14 @@ export interface ChoiceH<V> {
export function InputChoiceHorizontal<T extends object, K extends keyof T>(
props: {
- choices: ChoiceH<T[K]>[];
+ choices: ChoiceH<string>[];
} & 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);
+ const { choices, label, tooltip, help, required, converter } = props;
+ //FIXME: remove deprecated
+ const fieldCtx = useField<T, K>(props.name);
+ const { value, onChange, state } =
+ props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);
if (state.hidden) {
return <Fragment />;
}
@@ -45,7 +38,7 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>(
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) {
+ if (converter?.fromStringUI(choice.value as any) === value) {
clazz +=
" text-white bg-indigo-600 hover:bg-indigo-500 ring-2 ring-indigo-600 hover:ring-indigo-500";
} else {
@@ -62,12 +55,13 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>(
return (
<button
type="button"
+ key={idx}
disabled={state.disabled}
label={choice.label}
class={clazz}
onClick={(e) => {
onChange(
- (value === choice.value ? undefined : choice.value) as T[K],
+ (value === choice.value ? undefined : converter?.fromStringUI(choice.value as any)) as any,
);
}}
>
diff --git a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
index ed5170d17..9a634d05c 100644
--- a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,12 +43,12 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "choiceStacked",
- props: {
+ properties: {
label: "label of the field" as TranslatedString,
name: "comment",
choices: [{
diff --git a/packages/web-util/src/forms/InputChoiceStacked.tsx b/packages/web-util/src/forms/InputChoiceStacked.tsx
index 234bb2255..1928f4365 100644
--- a/packages/web-util/src/forms/InputChoiceStacked.tsx
+++ b/packages/web-util/src/forms/InputChoiceStacked.tsx
@@ -3,6 +3,7 @@ import { Fragment, VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
import { useField } from "./useField.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
export interface ChoiceS<V> {
label: TranslatedString;
@@ -27,7 +28,12 @@ export function InputChoiceStacked<T extends object, K extends keyof T>(
after,
converter,
} = props;
- const { value, onChange, state, isDirty } = useField<T, K>(name);
+
+ //FIXME: remove deprecated
+ const fieldCtx = useField<T, K>(props.name);
+ const { value, onChange, state } =
+ props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);
+
if (state.hidden) {
return <Fragment />;
}
@@ -41,7 +47,7 @@ export function InputChoiceStacked<T extends object, K extends keyof T>(
/>
<fieldset class="mt-2">
<div class="space-y-4">
- {choices.map((choice) => {
+ {choices.map((choice, idx) => {
// const currentValue = !converter
// ? choice.value
// : converter.fromStringUI(choice.value) ?? "";
@@ -56,7 +62,7 @@ export function InputChoiceStacked<T extends object, K extends keyof T>(
}
return (
- <label class={clazz}>
+ <label key={idx} class={clazz}>
<input
type="radio"
name="server-size"
@@ -71,7 +77,7 @@ export function InputChoiceStacked<T extends object, K extends keyof T>(
onChange(
(value === choice.value
? undefined
- : choice.value) as T[K],
+ : choice.value) as any,
);
}}
class="sr-only"
diff --git a/packages/web-util/src/forms/InputFile.stories.tsx b/packages/web-util/src/forms/InputFile.stories.tsx
index ba06debf9..eff18d071 100644
--- a/packages/web-util/src/forms/InputFile.stories.tsx
+++ b/packages/web-util/src/forms/InputFile.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,12 +43,12 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "file",
- props: {
+ properties: {
label: "label of the field" as TranslatedString,
name: "comment",
required: true,
diff --git a/packages/web-util/src/forms/InputFile.tsx b/packages/web-util/src/forms/InputFile.tsx
index 6337d0902..cd0a96d1c 100644
--- a/packages/web-util/src/forms/InputFile.tsx
+++ b/packages/web-util/src/forms/InputFile.tsx
@@ -1,5 +1,6 @@
import { Fragment, VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
import { useField } from "./useField.js";
@@ -7,20 +8,36 @@ export function InputFile<T extends object, K extends keyof T>(
props: { maxBites: number; accept?: string } & UIFormProps<T, K>,
): VNode {
const {
- name,
label,
- placeholder,
tooltip,
required,
help: propsHelp,
maxBites,
accept,
} = props;
- const { value, onChange, state } = useField<T, K>(name);
- const help = propsHelp ?? state.help
+ //FIXME: remove deprecated
+ const fieldCtx = useField<T, K>(props.name);
+ const { value, onChange, state } =
+ props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);
+
+ const help = propsHelp ?? state.help;
if (state.hidden) {
return <div />;
}
+
+ const valueStr = !value ? "" : value.toString();
+ const firstColon = valueStr.indexOf(";");
+
+ const { fileName, dataUri } = valueStr.startsWith("file:")
+ ? {
+ fileName: valueStr.substring(5, firstColon),
+ dataUri: valueStr.substring(firstColon + 1),
+ }
+ : {
+ fileName: "",
+ dataUri: valueStr,
+ };
+
return (
<div class="col-span-full">
<LabelWithTooltipMaybeRequired
@@ -28,7 +45,7 @@ export function InputFile<T extends object, K extends keyof T>(
tooltip={tooltip}
required={required}
/>
- {!value || !(value as string).startsWith("data:image/") ? (
+ {!dataUri ? (
<div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1">
<div class="text-center">
<svg
@@ -43,16 +60,15 @@ export function InputFile<T extends object, K extends keyof T>(
clip-rule="evenodd"
/>
</svg>
- {!state.disabled &&
+ {!state.disabled && (
<div class="my-2 flex text-sm leading-6 text-gray-600">
<label
- for="file-upload"
+ for={String(props.name)}
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"
+ id={String(props.name)}
type="file"
class="sr-only"
accept={accept}
@@ -64,6 +80,7 @@ export function InputFile<T extends object, K extends keyof T>(
if (f[0].size > maxBites) {
return onChange(undefined!);
}
+ const fileName = f[0].name;
return f[0].arrayBuffer().then((b) => {
const b64 = window.btoa(
new Uint8Array(b).reduce(
@@ -71,24 +88,40 @@ export function InputFile<T extends object, K extends keyof T>(
"",
),
);
- return onChange(`data:${f[0].type};base64,${b64}` as any);
+ if (fileName) {
+ return onChange(
+ `file:${fileName};data:${f[0].type};base64,${b64}` as any,
+ );
+ } else {
+ 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"
- />
+ {(dataUri as string).startsWith("data:image/") ? (
+ <img src={dataUri} class=" h-24 w-full object-cover relative" />
+ ) : (
+ <div />
+ )}
+ {fileName ? (
+ <div class="absolute rounded-lg border flex justify-center text-xl items-center text-white ">
+ {fileName}
+ </div>
+ ) : (
+ <Fragment />
+ )}
- {!state.disabled &&
+ {!state.disabled && (
<div
class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer "
onClick={() => {
@@ -97,7 +130,7 @@ export function InputFile<T extends object, K extends keyof T>(
>
Clear
</div>
- }
+ )}
</div>
)}
{help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>}
diff --git a/packages/web-util/src/forms/InputInteger.stories.tsx b/packages/web-util/src/forms/InputInteger.stories.tsx
index bd1a467ab..378736a24 100644
--- a/packages/web-util/src/forms/InputInteger.stories.tsx
+++ b/packages/web-util/src/forms/InputInteger.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -38,12 +38,12 @@ const initial: TargetObject = {
age: 5,
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "integer",
- props: {
+ properties: {
label: "label of the field" as TranslatedString,
name: "age",
tooltip: "just numbers" as TranslatedString,
diff --git a/packages/web-util/src/forms/InputLine.stories.tsx b/packages/web-util/src/forms/InputLine.stories.tsx
index da41a221e..dea5c142a 100644
--- a/packages/web-util/src/forms/InputLine.stories.tsx
+++ b/packages/web-util/src/forms/InputLine.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,12 +43,12 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "text",
- props: {
+ properties: {
label: "label of the field" as TranslatedString,
name: "comment",
},
diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx
index b8879f9ec..eb3238ef9 100644
--- a/packages/web-util/src/forms/InputLine.tsx
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -1,7 +1,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { UIFormProps } from "./FormProvider.js";
+import { Addon, UIFormProps } from "./FormProvider.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { useField } from "./useField.js";
//@ts-ignore
@@ -68,6 +68,37 @@ export function LabelWithTooltipMaybeRequired({
return WithTooltip;
}
+export function RenderAddon({ disabled, addon }: { disabled?: boolean, addon: Addon }): VNode {
+ switch (addon.type) {
+ case "text": {
+ return (
+ <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+ {addon.text}
+ </span>
+ );
+ }
+ case "icon": {
+ return (
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
+ {addon.icon}
+ </div>
+ );
+ }
+ case "button": {
+ return (
+ <button
+ type="button"
+ disabled={disabled}
+ onClick={addon.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"
+ >
+ {addon.children}
+ </button>
+ );
+ }
+ }
+}
+
function InputWrapper<T extends object, K extends keyof T>({
children,
label,
@@ -78,7 +109,11 @@ function InputWrapper<T extends object, K extends keyof T>({
error,
disabled,
required,
-}: { error?: string; disabled: boolean, children: ComponentChildren } & UIFormProps<T, K>): VNode {
+}: {
+ error?: string;
+ disabled: boolean;
+ children: ComponentChildren;
+} & UIFormProps<T, K>): VNode {
return (
<div class="sm:col-span-6">
<LabelWithTooltipMaybeRequired
@@ -87,47 +122,11 @@ function InputWrapper<T extends object, K extends keyof T>({
tooltip={tooltip}
/>
<div class="relative mt-2 flex rounded-md shadow-sm">
- {before &&
- (before.type === "text" ? (
- <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
- {before.text}
- </span>
- ) : before.type === "icon" ? (
- <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
- {before.icon}
- </div>
- ) : before.type === "button" ? (
- <button
- type="button"
- disabled={disabled}
- onClick={before.onClick}
- class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
- >
- {before.children}
- </button>
- ) : undefined)}
+ {before && <RenderAddon disabled={disabled} addon={before} />}
{children}
- {after &&
- (after.type === "text" ? (
- <span class="inline-flex items-center rounded-r-md border border-l-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
- {after.text}
- </span>
- ) : after.type === "icon" ? (
- <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
- {after.icon}
- </div>
- ) : after.type === "button" ? (
- <button
- type="button"
- disabled={disabled}
- onClick={after.onClick}
- class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
- >
- {after.children}
- </button>
- ) : undefined)}
+ {after && <RenderAddon disabled={disabled} addon={after} />}
</div>
{error && (
<p class="mt-2 text-sm text-red-600" id="email-error">
@@ -156,19 +155,22 @@ 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);
+ //FIXME: remove deprecated
+ const fieldCtx = useField<T, K>(props.name);
+ const { value, onChange, state, error } =
+ props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);
- const [text, setText] = useState("")
+ // const [text, setText] = useState("");
const fromString: (s: string) => any =
converter?.fromStringUI ?? defaultFromString;
const toString: (s: any) => string = converter?.toStringUI ?? defaultToString;
- useEffect(() => {
- const newValue = toString(value)
- if (newValue) {
- setText(newValue)
- }
- }, [value])
+ // useEffect(() => {
+ // const newValue = toString(value);
+ // if (newValue) {
+ // setText(newValue);
+ // }
+ // }, [value]);
if (state.hidden) return <div />;
@@ -206,7 +208,7 @@ export function InputLine<T extends object, K extends keyof T>(
}
}
}
- const showError = isDirty && state.error;
+ const showError = value !== undefined && error;
if (showError) {
clazz +=
" text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500";
@@ -221,7 +223,7 @@ export function InputLine<T extends object, K extends keyof T>(
{...props}
help={props.help ?? state.help}
disabled={state.disabled ?? false}
- error={showError ? state.error : undefined}
+ error={showError ? error : undefined}
>
<textarea
rows={4}
@@ -242,21 +244,23 @@ export function InputLine<T extends object, K extends keyof T>(
}
return (
- <InputWrapper<T, K> {...props}
+ <InputWrapper<T, K>
+ {...props}
help={props.help ?? state.help}
- disabled={state.disabled ?? false} error={showError ? state.error : undefined}
+ disabled={state.disabled ?? false}
+ error={showError ? error : undefined}
>
<input
name={String(name)}
type={type}
onChange={(e) => {
- setText(e.currentTarget.value)
+ onChange(fromString(e.currentTarget.value));
}}
placeholder={placeholder ? placeholder : undefined}
- value={text}
- onBlur={() => {
- onChange(fromString(text));
- }}
+ value={toString(value) ?? ""}
+ // onBlur={() => {
+ // onChange(fromString(value as any));
+ // }}
// defaultValue={toString(value)}
disabled={state.disabled}
aria-invalid={showError}
diff --git a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
index 6ce5445c0..ab17545f5 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -45,12 +45,12 @@ const initial: TargetObject = {
things: [],
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "selectMultiple",
- props: {
+ properties: {
label: "allow diplicates" as TranslatedString,
name: "pets",
placeholder: "search..." as TranslatedString,
@@ -67,7 +67,7 @@ const form: FlexibleForm<TargetObject> = {
},
}, {
type: "selectMultiple",
- props: {
+ properties: {
label: "unique values" as TranslatedString,
name: "things",
unique: true,
diff --git a/packages/web-util/src/forms/InputSelectMultiple.tsx b/packages/web-util/src/forms/InputSelectMultiple.tsx
index a67eb23b7..1bcf85061 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.tsx
@@ -1,6 +1,7 @@
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { UIFormProps } from "./FormProvider.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { ChoiceS } from "./InputChoiceStacked.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
import { useField } from "./useField.js";
@@ -12,23 +13,28 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
max?: number;
} & UIFormProps<T, K>,
): VNode {
- const { name, label, choices, placeholder, tooltip, required, unique, max } =
- props;
- const { value, onChange, state } = useField<T, K>(name);
+ const { converter, label, choices, placeholder, tooltip, required, unique, max } = props;
+ //FIXME: remove deprecated
+ const fieldCtx = useField<T, K>(props.name);
+ const { value, onChange, state } =
+ props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.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 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 regex.test(v.label);
+ });
return (
<div class="sm:col-span-6">
<LabelWithTooltipMaybeRequired
@@ -38,7 +44,10 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
/>
{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">
+ <span
+ key={idx}
+ 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"
@@ -46,7 +55,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
onClick={() => {
const newValue = [...list];
newValue.splice(idx, 1);
- onChange(newValue as T[K]);
+ onChange(newValue as any);
setFilter(undefined);
}}
class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
@@ -64,91 +73,94 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
);
})}
- {!state.disabled && <div class="relative mt-2">
- <input
- id="combobox"
- type="text"
- value={filter ?? ""}
- onChange={(e) => {
- setFilter(e.currentTarget.value);
- }}
- placeholder={placeholder}
- class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- role="combobox"
- aria-controls="options"
- aria-expanded="false"
- />
- <button
- type="button"
- disabled={state.disabled}
- onClick={() => {
- setFilter(filter === undefined ? "" : undefined);
- }}
- class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
- >
- <svg
- class="h-5 w-5 text-gray-400"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
+ {!state.disabled && (
+ <div class="relative mt-2">
+ <input
+ id="combobox"
+ type="text"
+ value={filter ?? ""}
+ onChange={(e) => {
+ setFilter(e.currentTarget.value);
+ }}
+ placeholder={placeholder}
+ class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ role="combobox"
+ aria-controls="options"
+ aria-expanded="false"
+ />
+ <button
+ type="button"
+ disabled={state.disabled}
+ onClick={() => {
+ setFilter(filter === undefined ? "" : undefined);
+ }}
+ class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
>
- <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>
+ <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]);
- }}
+ {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
+ key={idx}
+ 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 any);
+ }}
- // tabindex="-1"
- >
- {/* <!-- Selected: "font-semibold" --> */}
- <span class="block truncate">{v.label}</span>
+ // 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>
- );
- })}
+ </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>}
+ {/* <!-- More items... --> */}
+ </ul>
+ )}
+ </div>
+ )}
</div>
);
}
diff --git a/packages/web-util/src/forms/InputSelectOne.stories.tsx b/packages/web-util/src/forms/InputSelectOne.stories.tsx
index 9e9029a77..2ebde3096 100644
--- a/packages/web-util/src/forms/InputSelectOne.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectOne.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,12 +43,12 @@ const initial: TargetObject = {
things: "one"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "selectOne",
- props: {
+ properties: {
label: "label of the field" as TranslatedString,
name: "things",
placeholder: "search..." as TranslatedString,
diff --git a/packages/web-util/src/forms/InputSelectOne.tsx b/packages/web-util/src/forms/InputSelectOne.tsx
index d100b079d..26f887b08 100644
--- a/packages/web-util/src/forms/InputSelectOne.tsx
+++ b/packages/web-util/src/forms/InputSelectOne.tsx
@@ -4,27 +4,35 @@ import { UIFormProps } from "./FormProvider.js";
import { ChoiceS } from "./InputChoiceStacked.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
import { useField } from "./useField.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
export function InputSelectOne<T extends object, K extends keyof T>(
props: {
choices: ChoiceS<T[K]>[];
} & UIFormProps<T, K>,
): VNode {
- const { name, label, choices, placeholder, tooltip, required } = props;
- const { value, onChange } = useField<T, K>(name);
+ const { label, choices, placeholder, tooltip, required } = props;
+ //FIXME: remove deprecated
+ const fieldCtx = useField<T, K>(props.name);
+ const { value, onChange } =
+ props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.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 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 regex.test(v.label);
+ });
return (
<div class="sm:col-span-6">
<LabelWithTooltipMaybeRequired
@@ -97,15 +105,16 @@ export function InputSelectOne<T extends object, K extends keyof T>(
{filteredChoices.map((v, idx) => {
return (
<li
+ key={idx}
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]);
+ onChange(v.value as any);
}}
- // tabindex="-1"
+ // tabindex="-1"
>
{/* <!-- Selected: "font-semibold" --> */}
<span class="block truncate">{v.label}</span>
diff --git a/packages/web-util/src/forms/InputText.stories.tsx b/packages/web-util/src/forms/InputText.stories.tsx
index 04ab8a1c6..60b6ca224 100644
--- a/packages/web-util/src/forms/InputText.stories.tsx
+++ b/packages/web-util/src/forms/InputText.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,12 +43,12 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "text",
- props: {
+ properties: {
label: "label of the field" as TranslatedString,
name: "comment",
},
diff --git a/packages/web-util/src/forms/InputTextArea.stories.tsx b/packages/web-util/src/forms/InputTextArea.stories.tsx
index c8c3eb088..ab1a695f5 100644
--- a/packages/web-util/src/forms/InputTextArea.stories.tsx
+++ b/packages/web-util/src/forms/InputTextArea.stories.tsx
@@ -23,7 +23,7 @@ import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
DefaultForm as TestedComponent,
- FlexibleForm,
+ FlexibleForm_Deprecated,
} from "./DefaultForm.js";
export default {
@@ -43,12 +43,12 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "text",
- props: {
+ properties: {
label: "label of the field" as TranslatedString,
name: "comment",
},
diff --git a/packages/web-util/src/forms/InputToggle.stories.tsx b/packages/web-util/src/forms/InputToggle.stories.tsx
index ca6857618..fcc57ffe2 100644
--- a/packages/web-util/src/forms/InputToggle.stories.tsx
+++ b/packages/web-util/src/forms/InputToggle.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,12 +43,12 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
type: "toggle",
- props: {
+ properties: {
label: "label of the field" as TranslatedString,
name: "comment",
},
diff --git a/packages/web-util/src/forms/InputToggle.tsx b/packages/web-util/src/forms/InputToggle.tsx
index 56b29c502..58386045c 100644
--- a/packages/web-util/src/forms/InputToggle.tsx
+++ b/packages/web-util/src/forms/InputToggle.tsx
@@ -1,5 +1,6 @@
import { VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
import { useField } from "./useField.js";
@@ -17,22 +18,39 @@ export function InputToggle<T extends object, K extends keyof T>(
after,
converter,
} = props;
- const { value, onChange, state, isDirty } = useField<T, K>(name);
+ //FIXME: remove deprecated
+ const fieldCtx = useField<T, K>(props.name);
+ const { value, onChange } =
+ props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);
- const isOn = !!value
- return <div class="sm:col-span-6">
- <div class="flex items-center justify-between">
- <LabelWithTooltipMaybeRequired
- label={label}
- required={required}
- tooltip={tooltip}
- />
- <button type="button" data-enabled={isOn}
- class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
- role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
- onClick={() => { onChange(!isOn as any); }}>
- <span aria-hidden="true" data-enabled={isOn} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
- </button>
+ const isOn = !!value;
+ return (
+ <div class="sm:col-span-6">
+ <div class="flex items-center justify-between">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ required={required}
+ tooltip={tooltip}
+ />
+ <button
+ type="button"
+ data-enabled={isOn}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ onChange(!isOn as any);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={isOn}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
</div>
- </div>
+ );
}
diff --git a/packages/web-util/src/forms/converter.ts b/packages/web-util/src/forms/converter.ts
new file mode 100644
index 000000000..3a522bf7e
--- /dev/null
+++ b/packages/web-util/src/forms/converter.ts
@@ -0,0 +1,119 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ TalerExchangeApi,
+} from "@gnu-taler/taler-util";
+import { format, parse } from "date-fns";
+import { StringConverter } from "./FormProvider.js";
+
+export const amlStateConverter = {
+ toStringUI: stringifyAmlState,
+ fromStringUI: parseAmlState,
+};
+
+function stringifyAmlState(s: TalerExchangeApi.AmlState | undefined): string {
+ if (s === undefined) return "";
+ switch (s) {
+ case TalerExchangeApi.AmlState.normal:
+ return "normal";
+ case TalerExchangeApi.AmlState.pending:
+ return "pending";
+ case TalerExchangeApi.AmlState.frozen:
+ return "frozen";
+ }
+}
+
+function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState {
+ switch (s) {
+ case "normal":
+ return TalerExchangeApi.AmlState.normal;
+ case "pending":
+ return TalerExchangeApi.AmlState.pending;
+ case "frozen":
+ return TalerExchangeApi.AmlState.frozen;
+ default:
+ throw Error(`unknown AML state: ${s}`);
+ }
+}
+
+function amountConverter(config: any): StringConverter<AmountJson> {
+ const currency = config["currency"];
+ if (!currency || typeof currency !== "string") {
+ throw Error(`amount converter needs a currency`);
+ }
+ return {
+ fromStringUI(v: string | undefined): AmountJson {
+ // FIXME: requires currency
+ return Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency);
+ },
+ toStringUI(v: unknown): string {
+ return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson);
+ },
+ };
+}
+
+function absTimeConverter(config: any): StringConverter<AbsoluteTime> {
+ const pattern = config["pattern"];
+ if (!pattern || typeof pattern !== "string") {
+ throw Error(`absTime converter needs a pattern`);
+ }
+ return {
+ fromStringUI(v: string | undefined): AbsoluteTime {
+ if (v === undefined) {
+ return AbsoluteTime.never();
+ }
+ try {
+ const time = parse(v, pattern, new Date());
+ return AbsoluteTime.fromMilliseconds(time.getTime());
+ } catch(e) {
+ return AbsoluteTime.never();
+ }
+ },
+ toStringUI(v: unknown): string {
+ if (v === undefined) return "";
+ const d = v as AbsoluteTime;
+ if (d.t_ms === "never") return "never";
+ try {
+ return format(d.t_ms, pattern)
+ } catch (e) {
+ return ""
+ }
+ },
+ };
+}
+
+export function getConverterById(
+ id: string | undefined,
+ config: unknown,
+): StringConverter<unknown> {
+ if (id === "Taler.AbsoluteTime") {
+ // @ts-expect-error check this
+ return absTimeConverter(config);
+ }
+ if (id === "Taler.Amount") {
+ // @ts-expect-error check this
+ return amountConverter(config);
+ }
+ if (id === "TalerExchangeApi.AmlState") {
+ // @ts-expect-error check this
+ return amlStateConverter;
+ }
+ return undefined!;
+}
diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts
index 3b8620bfb..cb2ee0145 100644
--- a/packages/web-util/src/forms/forms.ts
+++ b/packages/web-util/src/forms/forms.ts
@@ -1,6 +1,5 @@
import { h as create, Fragment, VNode } from "preact";
import { Caption } from "./Caption.js";
-import { FormProvider } from "./FormProvider.js";
import { Group } from "./Group.js";
import { InputAbsoluteTime } from "./InputAbsoluteTime.js";
import { InputAmount } from "./InputAmount.js";
@@ -9,13 +8,15 @@ import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
import { InputChoiceStacked } from "./InputChoiceStacked.js";
import { InputFile } from "./InputFile.js";
import { InputInteger } from "./InputInteger.js";
-import { InputLine } from "./InputLine.js";
import { InputSelectMultiple } from "./InputSelectMultiple.js";
import { InputSelectOne } from "./InputSelectOne.js";
import { InputText } from "./InputText.js";
import { InputTextArea } from "./InputTextArea.js";
import { InputToggle } from "./InputToggle.js";
-
+import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js";
+import { InternationalizationAPI, UIFieldElementDescription } from "../index.browser.js";
+import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util";
+import {UIFormFieldBaseConfig, UIFormElementConfig} from "./ui-form.js";
/**
* Constrain the type with the ui props
*/
@@ -30,7 +31,7 @@ type FieldType<T extends object = any, K extends keyof T = any> = {
textArea: Parameters<typeof InputTextArea<T, K>>[0];
choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
- absoluteTime: Parameters<typeof InputAbsoluteTime<T, K>>[0];
+ absoluteTimeText: Parameters<typeof InputAbsoluteTime<T, K>>[0];
integer: Parameters<typeof InputInteger<T, K>>[0];
toggle: Parameters<typeof InputToggle<T, K>>[0];
amount: Parameters<typeof InputAmount<T, K>>[0];
@@ -40,20 +41,32 @@ type FieldType<T extends object = any, K extends keyof T = any> = {
* List all the form fields so typescript can type-check the form instance
*/
export type UIFormField =
- | { type: "group"; props: FieldType["group"] }
- | { type: "caption"; props: FieldType["caption"] }
- | { type: "array"; props: FieldType["array"] }
- | { type: "file"; props: FieldType["file"] }
- | { type: "amount"; props: FieldType["amount"] }
- | { type: "selectOne"; props: FieldType["selectOne"] }
- | { type: "selectMultiple"; props: FieldType["selectMultiple"] }
- | { type: "text"; props: FieldType["text"] }
- | { type: "textArea"; props: FieldType["textArea"] }
- | { type: "choiceStacked"; props: FieldType["choiceStacked"] }
- | { type: "choiceHorizontal"; props: FieldType["choiceHorizontal"] }
- | { type: "integer"; props: FieldType["integer"] }
- | { type: "toggle"; props: FieldType["toggle"] }
- | { type: "absoluteTime"; props: FieldType["absoluteTime"] };
+ | { type: "group"; properties: FieldType["group"] }
+ | { type: "caption"; properties: FieldType["caption"] }
+ | { type: "array"; properties: FieldType["array"] }
+ | { type: "file"; properties: FieldType["file"] }
+ | { type: "amount"; properties: FieldType["amount"] }
+ | { type: "selectOne"; properties: FieldType["selectOne"] }
+ | {
+ type: "selectMultiple";
+ properties: FieldType["selectMultiple"];
+ }
+ | { type: "text"; properties: FieldType["text"] }
+ | { type: "textArea"; properties: FieldType["textArea"] }
+ | {
+ type: "choiceStacked";
+ properties: FieldType["choiceStacked"];
+ }
+ | {
+ type: "choiceHorizontal";
+ properties: FieldType["choiceHorizontal"];
+ }
+ | { type: "integer"; properties: FieldType["integer"] }
+ | { type: "toggle"; properties: FieldType["toggle"] }
+ | {
+ type: "absoluteTimeText";
+ properties: FieldType["absoluteTimeText"];
+ };
type FieldComponentFunction<key extends keyof FieldType> = (
props: FieldType[key],
@@ -76,7 +89,7 @@ const UIFormConfiguration: UIFormFieldMap = {
file: InputFile,
textArea: InputTextArea,
//@ts-ignore
- absoluteTime: InputAbsoluteTime,
+ absoluteTimeText: InputAbsoluteTime,
//@ts-ignore
choiceStacked: InputChoiceStacked,
//@ts-ignore
@@ -104,31 +117,256 @@ export function RenderAllFieldsByUiConfig({
const Component = UIFormConfiguration[
field.type
] as FieldComponentFunction<any>;
- return Component(field.props);
+ return Component(field.properties);
}),
);
}
-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>;
-};
+// type FormSet<T extends object> = {
+// Provider: typeof FormProvider<T>;
+// InputLine: <K extends keyof T>() => typeof InputLine<T, K>;
+// InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal<T, K>;
+// };
/**
* Helper function that created a typed object.
+ *
+ * @returns
+ */
+// export function createNewForm<T extends object>() {
+// const res: FormSet<T> = {
+// Provider: FormProvider,
+// InputLine: () => InputLine,
+// InputChoiceHorizontal: () => InputChoiceHorizontal,
+// };
+// return {
+// Provider: res.Provider,
+// InputLine: res.InputLine(),
+// InputChoiceHorizontal: res.InputChoiceHorizontal(),
+// };
+// }
+
+/**
+ * convert field configuration to render function
*
+ * @param i18n_
+ * @param fieldConfig
+ * @param form
* @returns
*/
-export function createNewForm<T extends object>() {
- const res: FormSet<T> = {
- Provider: FormProvider,
- InputLine: () => InputLine,
- InputChoiceHorizontal: () => InputChoiceHorizontal,
+export function convertUiField(
+ i18n_: InternationalizationAPI,
+ fieldConfig: UIFormElementConfig[],
+ form: object,
+ getConverterById: GetConverterById,
+): UIFormField[] {
+ return fieldConfig.map((config) => {
+ // NON input fields
+ switch (config.type) {
+ case "caption": {
+ const resp: UIFormField = {
+ type: config.type,
+ properties: converBaseFieldsProps(i18n_, config),
+ };
+ return resp;
+ }
+ case "group": {
+ const resp: UIFormField = {
+ type: config.type,
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ fields: convertUiField(i18n_, config.fields, form, getConverterById),
+ },
+ };
+ return resp;
+ }
+ }
+ // Input Fields
+ switch (config.type) {
+ case "array": {
+ return {
+ type: "array",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ labelField: config.labelFieldId,
+ fields: convertUiField(i18n_, config.fields, form, getConverterById),
+ },
+ } as UIFormField;
+ }
+ case "absoluteTimeText": {
+ return {
+ type: "absoluteTimeText",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ },
+ } as UIFormField;
+ }
+ case "amount": {
+ return {
+ type: "amount",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ currency: config.currency,
+ },
+ } as UIFormField;
+ }
+ case "choiceHorizontal": {
+ return {
+ type: "choiceHorizontal",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ choices: config.choices,
+ },
+ } as UIFormField;
+ }
+ case "choiceStacked": {
+ return {
+ type: "choiceStacked",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ choices: config.choices,
+
+ },
+ }as UIFormField;
+ }
+ case "file":{
+ return {
+ type: "file",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ accept: config.accept,
+ maxBites: config.maxBytes,
+ },
+ } as UIFormField;
+ }
+ case "integer":{
+ return {
+ type: "integer",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ },
+ } as UIFormField;
+ }
+ case "selectMultiple":{
+ return {
+ type: "selectMultiple",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ choices: config.choices,
+ },
+ } as UIFormField;
+ }
+ case "selectOne": {
+ return {
+ type: "selectOne",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ choices: config.choices,
+ },
+ } as UIFormField;
+ }
+ case "text": {
+ return {
+ type: "text",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ },
+ } as UIFormField;
+ }
+ case "textArea": {
+ return {
+ type: "text",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ },
+ } as UIFormField;
+ }
+ case "toggle": {
+ return {
+ type: "toggle",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(form, config, getConverterById),
+ },
+ } as UIFormField;
+ }
+ default: {
+ assertUnreachable(config);
+ }
+ }
+ });
+}
+
+
+
+function getAddonById(_id: string | undefined): Addon {
+ return undefined!;
+}
+
+
+type GetConverterById = (
+ id: string | undefined,
+ config: unknown,
+) => StringConverter<unknown>;
+
+
+function converInputFieldsProps(
+ form: object,
+ p: UIFormFieldBaseConfig,
+ getConverterById: GetConverterById,
+) {
+ return {
+ converter: getConverterById(p.converterId, p),
+ handler: getValueDeeper2(form, p.id.split(".")),
+ name: p.name,
+ required: p.required,
+ disabled: p.disabled,
+ help: p.help,
+ placeholder: p.placeholder,
+ tooltip: p.tooltip,
+ label: p.label as TranslatedString,
};
+}
+
+function converBaseFieldsProps(
+ i18n_: InternationalizationAPI,
+ p: UIFieldElementDescription,
+) {
return {
- Provider: res.Provider,
- InputLine: res.InputLine(),
- InputChoiceHorizontal: res.InputChoiceHorizontal(),
+ after: getAddonById(p.addonAfterId),
+ before: getAddonById(p.addonBeforeId),
+ hidden: p.hidden,
+ name: p.name,
+ help: i18n_.str`${p.help}`,
+ label: i18n_.str`${p.label}`,
+ tooltip: i18n_.str`${p.tooltip}`,
};
}
+
+function getValueDeeper2(
+ object: Record<string, any>,
+ names: string[],
+): UIFieldHandler {
+ if (names.length === 0) return object as UIFieldHandler;
+ const [head, ...rest] = names;
+ if (!head) {
+ return getValueDeeper2(object, rest);
+ }
+ if (object === undefined) {
+ throw Error("handler not found");
+ }
+ return getValueDeeper2(object[head], rest);
+}
+
+
diff --git a/packages/web-util/src/forms/index.ts b/packages/web-util/src/forms/index.ts
index 4ff71f197..7320c70d0 100644
--- a/packages/web-util/src/forms/index.ts
+++ b/packages/web-util/src/forms/index.ts
@@ -19,5 +19,7 @@ export * from "./InputTextArea.js"
export * from "./InputToggle.js"
export * from "./TimePicker.js"
export * from "./forms.js"
+export * from "./ui-form.js"
+export * from "./converter.js"
export * from "./useField.js"
diff --git a/packages/web-util/src/forms/ui-form.ts b/packages/web-util/src/forms/ui-form.ts
new file mode 100644
index 000000000..012499d6d
--- /dev/null
+++ b/packages/web-util/src/forms/ui-form.ts
@@ -0,0 +1,363 @@
+import {
+ buildCodecForObject,
+ buildCodecForUnion,
+ Codec,
+ codecForBoolean,
+ codecForConstString,
+ codecForLazy,
+ codecForList,
+ codecForNumber,
+ codecForString,
+ codecForTimestamp,
+ codecOptional,
+ Integer,
+ TalerProtocolTimestamp,
+} from "@gnu-taler/taler-util";
+
+export type FormConfiguration = DoubleColumnForm;
+
+export type DoubleColumnForm = {
+ type: "double-column";
+ design: DoubleColumnFormSection[];
+ // behavior?: (form: Partial<T>) => FormState<T>;
+};
+
+export type DoubleColumnFormSection = {
+ title: string;
+ description?: string;
+ fields: UIFormElementConfig[];
+};
+
+// export interface BaseForm {
+// state: TalerExchangeApi.AmlState;
+// threshold: AmountJson;
+// }
+
+export type UIFormElementConfig =
+ | UIFormElementGroup
+ | UIFormElementCaption
+ | UIFormFieldAbsoluteTime
+ | UIFormFieldAmount
+ | UIFormFieldArray
+ | UIFormFieldChoiseHorizontal
+ | UIFormFieldChoiseStacked
+ | UIFormFieldFile
+ | UIFormFieldInteger
+ | UIFormFieldSelectMultiple
+ | UIFormFieldSelectOne
+ | UIFormFieldText
+ | UIFormFieldTextArea
+ | UIFormFieldToggle;
+
+type UIFormFieldAbsoluteTime = {
+ type: "absoluteTimeText";
+ max?: TalerProtocolTimestamp;
+ min?: TalerProtocolTimestamp;
+ pattern: string;
+} & UIFormFieldBaseConfig;
+
+type UIFormFieldAmount = {
+ type: "amount";
+ max?: Integer;
+ min?: Integer;
+ currency: string;
+} & UIFormFieldBaseConfig;
+
+type UIFormFieldArray = {
+ type: "array";
+ // id of the field shown when the array is collapsed
+ labelFieldId: UIHandlerId;
+ fields: UIFormElementConfig[];
+} & UIFormFieldBaseConfig;
+
+type UIFormElementCaption = { type: "caption" } & UIFieldElementDescription;
+
+type UIFormElementGroup = {
+ type: "group";
+ fields: UIFormElementConfig[];
+} & UIFieldElementDescription;
+
+type UIFormFieldChoiseHorizontal = {
+ type: "choiceHorizontal";
+ choices: Array<SelectUiChoice>;
+} & UIFormFieldBaseConfig;
+
+type UIFormFieldChoiseStacked = {
+ type: "choiceStacked";
+ choices: Array<SelectUiChoice>;
+} & UIFormFieldBaseConfig;
+
+type UIFormFieldFile = {
+ type: "file";
+ maxBytes?: Integer;
+ minBytes?: Integer;
+ // comma-separated list of one or more file types
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers
+ accept?: string;
+} & UIFormFieldBaseConfig;
+
+type UIFormFieldInteger = {
+ type: "integer";
+ max?: Integer;
+ min?: Integer;
+} & UIFormFieldBaseConfig;
+
+interface SelectUiChoice {
+ label: string;
+ description?: string;
+ value: string;
+}
+
+type UIFormFieldSelectMultiple = {
+ type: "selectMultiple";
+ max?: Integer;
+ min?: Integer;
+ unique?: boolean;
+ choices: Array<SelectUiChoice>;
+} & UIFormFieldBaseConfig;
+
+type UIFormFieldSelectOne = {
+ type: "selectOne";
+ choices: Array<SelectUiChoice>;
+} & UIFormFieldBaseConfig;
+type UIFormFieldText = { type: "text" } & UIFormFieldBaseConfig;
+type UIFormFieldTextArea = { type: "textArea" } & UIFormFieldBaseConfig;
+type UIFormFieldToggle = { type: "toggle" } & UIFormFieldBaseConfig;
+
+export type UIFieldElementDescription = {
+ /* label if the field, visible for the user */
+ label: string;
+
+ /* long text to be shown on user demand */
+ tooltip?: string;
+
+ /* short text to be shown close to the field, usually below and dimmer*/
+ help?: string;
+
+ /* name of the field, useful for a11y */
+ name: string;
+
+ /* if the field should be initially hidden */
+ hidden?: boolean;
+
+ /* ui element to show before */
+ addonBeforeId?: string;
+
+ /* ui element to show after */
+ addonAfterId?: string;
+};
+
+export type UIFormFieldBaseConfig = UIFieldElementDescription & {
+ /* example to be shown inside the field */
+ placeholder?: string;
+
+ /* show a mark as required */
+ required?: boolean;
+
+ /* readonly and dim */
+ disabled?: boolean;
+
+ /* conversion id to convert the string into the value type
+ the id should be known to the ui impl
+ */
+ converterId?: string;
+
+ /* property id of the form */
+ id: UIHandlerId;
+};
+
+declare const __handlerId: unique symbol;
+export type UIHandlerId = string & { [__handlerId]: true };
+
+// FIXME: validate well formed ui field id
+const codecForUiFieldId = codecForString as () => Codec<UIHandlerId>;
+
+const codecForUIFormFieldBaseDescriptionTemplate = <
+ T extends UIFieldElementDescription,
+>() =>
+ buildCodecForObject<T>()
+ .property("addonAfterId", codecOptional(codecForString()))
+ .property("addonBeforeId", codecOptional(codecForString()))
+ .property("hidden", codecOptional(codecForBoolean()))
+ .property("help", codecOptional(codecForString()))
+ .property("label", codecForString())
+ .property("name", codecForString())
+ .property("tooltip", codecOptional(codecForString()));
+
+const codecForUIFormFieldBaseConfigTemplate = <
+ T extends UIFormFieldBaseConfig,
+>() =>
+ codecForUIFormFieldBaseDescriptionTemplate<T>()
+ .property("id", codecForUiFieldId())
+ .property("converterId", codecOptional(codecForString()))
+ .property("disabled", codecOptional(codecForBoolean()))
+ .property("required", codecOptional(codecForBoolean()))
+ .property("placeholder", codecOptional(codecForString()));
+
+const codecForUiFormFieldAbsoluteTime = (): Codec<UIFormFieldAbsoluteTime> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldAbsoluteTime>()
+ .property("type", codecForConstString("absoluteTimeText"))
+ .property("pattern", codecForString())
+ .property("max", codecOptional(codecForTimestamp))
+ .property("min", codecOptional(codecForTimestamp))
+ .build("UIFormFieldAbsoluteTime");
+
+const codecForUiFormFieldAmount = (): Codec<UIFormFieldAmount> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldAmount>()
+ .property("type", codecForConstString("amount"))
+ .property("currency", codecForString())
+ .property("max", codecOptional(codecForNumber()))
+ .property("min", codecOptional(codecForNumber()))
+ .build("UIFormFieldAmount");
+
+const codecForUiFormFieldArray = (): Codec<UIFormFieldArray> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldArray>()
+ .property("type", codecForConstString("array"))
+ .property("labelFieldId", codecForUiFieldId())
+ .property("tooltip", codecOptional(codecForString()))
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ .property("fields", codecForList(codecForUiFormField()))
+ .build("UIFormFieldArray");
+
+const codecForUiFormFieldCaption = (): Codec<UIFormElementCaption> =>
+ codecForUIFormFieldBaseDescriptionTemplate<UIFormElementCaption>()
+ .property("type", codecForConstString("caption"))
+ .build("UIFormFieldCaption");
+
+const codecForUiFormSelectUiChoice = (): Codec<SelectUiChoice> =>
+ buildCodecForObject<SelectUiChoice>()
+ .property("description", codecOptional(codecForString()))
+ .property("label", codecForString())
+ .property("value", codecForString())
+ .build("SelectUiChoice");
+
+const codecForUiFormFieldChoiceHorizontal =
+ (): Codec<UIFormFieldChoiseHorizontal> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldChoiseHorizontal>()
+ .property("type", codecForConstString("choiceHorizontal"))
+ .property("choices", codecForList(codecForUiFormSelectUiChoice()))
+ .build("UIFormFieldChoiseHorizontal");
+
+const codecForUiFormFieldChoiceStacked = (): Codec<UIFormFieldChoiseStacked> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldChoiseStacked>()
+ .property("type", codecForConstString("choiceStacked"))
+ .property("choices", codecForList(codecForUiFormSelectUiChoice()))
+ .build("UIFormFieldChoiseStacked");
+
+const codecForUiFormFieldFile = (): Codec<UIFormFieldFile> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldFile>()
+ .property("type", codecForConstString("file"))
+ .property("accept", codecOptional(codecForString()))
+ .property("maxBytes", codecOptional(codecForNumber()))
+ .property("minBytes", codecOptional(codecForNumber()))
+ .build("UIFormFieldFile");
+
+const codecForUiFormFieldGroup = (): Codec<UIFormElementGroup> =>
+ codecForUIFormFieldBaseDescriptionTemplate<UIFormElementGroup>()
+ .property("type", codecForConstString("group"))
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ .property("fields", codecForList(codecForUiFormField()))
+ .build("UiFormFieldGroup");
+
+const codecForUiFormFieldInteger = (): Codec<UIFormFieldInteger> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldInteger>()
+ .property("type", codecForConstString("integer"))
+ // .property("properties", codecForUIFormFieldBaseConfig())
+ .property("max", codecOptional(codecForNumber()))
+ .property("min", codecOptional(codecForNumber()))
+ .build("UIFormFieldInteger");
+
+const codecForUiFormFieldSelectMultiple =
+ (): Codec<UIFormFieldSelectMultiple> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectMultiple>()
+ .property("type", codecForConstString("selectMultiple"))
+ .property("max", codecOptional(codecForNumber()))
+ .property("min", codecOptional(codecForNumber()))
+ .property("unique", codecOptional(codecForBoolean()))
+ .property("choices", codecForList(codecForUiFormSelectUiChoice()))
+ .build("UiFormFieldSelectMultiple");
+
+const codecForUiFormFieldSelectOne = (): Codec<UIFormFieldSelectOne> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectOne>()
+ .property("type", codecForConstString("selectOne"))
+ .property("choices", codecForList(codecForUiFormSelectUiChoice()))
+ .build("UIFormFieldSelectOne");
+
+const codecForUiFormFieldText = (): Codec<UIFormFieldText> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldText>()
+ .property("type", codecForConstString("text"))
+ .build("UIFormFieldText");
+
+const codecForUiFormFieldTextArea = (): Codec<UIFormFieldTextArea> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldTextArea>()
+ .property("type", codecForConstString("textArea"))
+ .build("UIFormFieldTextArea");
+
+const codecForUiFormFieldToggle = (): Codec<UIFormFieldToggle> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldToggle>()
+ .property("type", codecForConstString("toggle"))
+ .build("UIFormFieldToggle");
+
+const codecForUiFormField = (): Codec<UIFormElementConfig> =>
+ buildCodecForUnion<UIFormElementConfig>()
+ .discriminateOn("type")
+ .alternative("array", codecForLazy(codecForUiFormFieldArray))
+ .alternative("group", codecForLazy(codecForUiFormFieldGroup))
+ .alternative("absoluteTimeText", codecForUiFormFieldAbsoluteTime())
+ .alternative("amount", codecForUiFormFieldAmount())
+ .alternative("caption", codecForUiFormFieldCaption())
+ .alternative("choiceHorizontal", codecForUiFormFieldChoiceHorizontal())
+ .alternative("choiceStacked", codecForUiFormFieldChoiceStacked())
+ .alternative("file", codecForUiFormFieldFile())
+ .alternative("integer", codecForUiFormFieldInteger())
+ .alternative("selectMultiple", codecForUiFormFieldSelectMultiple())
+ .alternative("selectOne", codecForUiFormFieldSelectOne())
+ .alternative("text", codecForUiFormFieldText())
+ .alternative("textArea", codecForUiFormFieldTextArea())
+ .alternative("toggle", codecForUiFormFieldToggle())
+ .build("UIFormField");
+
+const codecForDoubleColumnFormSection = (): Codec<DoubleColumnFormSection> =>
+ buildCodecForObject<DoubleColumnFormSection>()
+ .property("title", codecForString())
+ .property("description", codecOptional(codecForString()))
+ .property("fields", codecForList(codecForUiFormField()))
+ .build("DoubleColumnFormSection");
+
+const codecForDoubleColumnForm = (): Codec<DoubleColumnForm> =>
+ buildCodecForObject<DoubleColumnForm>()
+ .property("type", codecForConstString("double-column"))
+ .property("design", codecForList(codecForDoubleColumnFormSection()))
+ .build("DoubleColumnForm");
+
+const codecForFormConfiguration = (): Codec<FormConfiguration> =>
+ buildCodecForUnion<FormConfiguration>()
+ .discriminateOn("type")
+ .alternative("double-column", codecForDoubleColumnForm())
+ .build<FormConfiguration>("FormConfiguration");
+
+const codecForFormMetadata = (): Codec<FormMetadata> =>
+ buildCodecForObject<FormMetadata>()
+ .property("label", codecForString())
+ .property("id", codecForString())
+ .property("version", codecForNumber())
+ .property("config", codecForFormConfiguration())
+ .build("FormMetadata");
+
+export const codecForUIForms = (): Codec<UiForms> =>
+ buildCodecForObject<UiForms>()
+ .property("forms", codecForList(codecForFormMetadata()))
+ .build("UiForms");
+
+export type FormMetadata = {
+ label: string;
+ id: string;
+ version: number;
+ config: FormConfiguration;
+};
+
+export interface UiForms {
+ // Where libeufin backend is localted
+ // default: window.origin without "webui/"
+ forms: Array<FormMetadata>;
+}
diff --git a/packages/web-util/src/forms/useField.ts b/packages/web-util/src/forms/useField.ts
index eed8cebea..a250d3100 100644
--- a/packages/web-util/src/forms/useField.ts
+++ b/packages/web-util/src/forms/useField.ts
@@ -1,30 +1,40 @@
-import { useContext, useState } from "preact/compat";
+import { useContext } from "preact/compat";
import { FieldUIOptions, FormContext } from "./FormProvider.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
export interface InputFieldHandler<Type> {
value: Type;
onChange: (s: Type) => void;
state: FieldUIOptions;
- isDirty: boolean;
+ error?: TranslatedString | undefined;
}
+/**
+ * @deprecated removing this so we don't depend on context to create a form
+ * @param name
+ * @returns
+ */
export function useField<T extends object, K extends keyof T>(
name: K,
-): InputFieldHandler<T[K]> {
+): InputFieldHandler<T[K]> | undefined {
+ const ctx = useContext(FormContext);
+ if (!ctx) {
+ //no context, can't be used
+ return undefined;
+ }
const {
value: formValue,
computeFormState,
onUpdate: notifyUpdate,
readOnly: readOnlyForm,
- } = useContext(FormContext);
+ } = ctx
type P = typeof name;
type V = T[P];
const formState = computeFormState ? computeFormState(formValue.current) : {};
const fieldValue = readField(formValue.current, String(name)) as V;
- // console.log("USE FIELD", String(name), formValue.current, fieldValue);
- const [currentValue, setCurrentValue] = useState<any | undefined>(fieldValue);
+
const fieldState =
readField<Partial<FieldUIOptions>>(formState, String(name)) ?? {};
@@ -32,13 +42,12 @@ export function useField<T extends object, K extends keyof T>(
const state = {
disabled: readOnlyForm ? true : (fieldState.disabled ?? false),
hidden: fieldState.hidden ?? false,
- error: fieldState.error,
help: fieldState.help,
elements: "elements" in fieldState ? fieldState.elements ?? [] : [],
};
function onChange(value: V): void {
- setCurrentValue(value);
+ // setCurrentValue(value);
formValue.current = setValueDeeper(
formValue.current,
String(name).split("."),
@@ -52,7 +61,6 @@ export function useField<T extends object, K extends keyof T>(
return {
value: fieldValue,
onChange,
- isDirty: currentValue !== undefined,
state,
};
}
@@ -67,18 +75,8 @@ export function useField<T extends object, K extends keyof T>(
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);
}
diff --git a/packages/web-util/src/hooks/index.ts b/packages/web-util/src/hooks/index.ts
index f6c74ff22..ba1b6e222 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,5 +1,5 @@
export { useLang } from "./useLang.js";
-export { useLocalStorage, buildStorageKey } from "./useLocalStorage.js";
+export { useLocalStorage, buildStorageKey, StorageKey, StorageState } from "./useLocalStorage.js";
export { useMemoryStorage } from "./useMemoryStorage.js";
export * from "./useNotifications.js";
export {
diff --git a/packages/web-util/src/hooks/useLocalStorage.ts b/packages/web-util/src/hooks/useLocalStorage.ts
index 7c41f98be..abd80bacc 100644
--- a/packages/web-util/src/hooks/useLocalStorage.ts
+++ b/packages/web-util/src/hooks/useLocalStorage.ts
@@ -61,9 +61,9 @@ const supportLocalStorage = typeof window !== "undefined";
const supportBrowserStorage =
typeof chrome !== "undefined" && typeof chrome.storage !== "undefined";
- /**
- * Build setting storage
- */
+/**
+ * Build setting storage
+ */
const storage: ObservableMap<string, string> = (function buildStorage() {
if (supportBrowserStorage) {
//browser storage is like local storage but
@@ -83,7 +83,6 @@ const storage: ObservableMap<string, string> = (function buildStorage() {
return memoryMap<string>();
}
})();
-
//with initial value
export function useLocalStorage<Type = string>(
key: StorageKey<Type>,
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
index 99f4f2699..103b88c86 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -1,6 +1,7 @@
import {
AbsoluteTime,
Duration,
+ OperationAlternative,
OperationFail,
OperationOk,
OperationResult,
@@ -9,7 +10,7 @@ import {
TranslatedString,
} from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
-import { ButtonHandler } from "../components/Button.js";
+import { ButtonHandler, OnOperationFailReturnType, OnOperationSuccesReturnType } from "../components/Button.js";
import {
InternationalizationAPI,
memoryMap,
@@ -207,14 +208,8 @@ export function useLocalNotification(): [
type HandlerMaker = <T extends OperationResult<A, B>, A, B>(
onClick: () => Promise<T | undefined>,
- onOperationSuccess:
- | ((result: T extends OperationOk<any> ? T : never) => void)
- | ((
- result: T extends OperationOk<any> ? T : never,
- ) => TranslatedString | undefined),
- onOperationFail: (
- d: T extends OperationFail<any> ? T : never,
- ) => TranslatedString,
+ onOperationSuccess: OnOperationSuccesReturnType<T>,
+ onOperationFail?: OnOperationFailReturnType<T>,
onOperationComplete?: () => void,
) => ButtonHandler<T, A, B>;
@@ -235,14 +230,8 @@ export function useLocalNotificationHandler(): [
function makeHandler<T extends OperationResult<A, B>, A, B>(
onClick: () => Promise<T | undefined>,
- onOperationSuccess:
- | ((result: T extends OperationOk<any> ? T : never) => void)
- | ((
- result: T extends OperationOk<any> ? T : never,
- ) => TranslatedString | undefined),
- onOperationFail: (
- d: T extends OperationFail<any> ? T : never,
- ) => TranslatedString,
+ onOperationSuccess:OnOperationSuccesReturnType<T>,
+ onOperationFail?: OnOperationFailReturnType<T>,
onOperationComplete?: () => void,
): ButtonHandler<T, A, B> {
return {
diff --git a/packages/web-util/src/tests/mock.ts b/packages/web-util/src/tests/mock.ts
index f4eb0e7aa..d09e8b4a6 100644
--- a/packages/web-util/src/tests/mock.ts
+++ b/packages/web-util/src/tests/mock.ts
@@ -15,7 +15,6 @@
*/
import { Logger } from "@gnu-taler/taler-util";
-import { deprecate } from "util";
type HttpMethod =
| "get"
@@ -39,6 +38,9 @@ type HttpMethod =
| "unlink"
| "UNLINK";
+/**
+ * @deprecated do not use it, it will be removed
+ */
export type Query<Req, Res> = {
method: HttpMethod;
url: string;
@@ -69,6 +71,9 @@ type MockedResponse = {
expectedQuery?: ExpectationValues;
};
+/**
+ * @deprecated do not use it, it will be removed
+ */
export abstract class MockEnvironment {
expectations: Array<ExpectationValues> = [];
queriesMade: Array<ExpectationValues> = [];
diff --git a/packages/web-util/src/tests/swr.ts b/packages/web-util/src/tests/swr.ts
index 903cd48d8..d5f4341f3 100644
--- a/packages/web-util/src/tests/swr.ts
+++ b/packages/web-util/src/tests/swr.ts
@@ -28,6 +28,7 @@ const logger = new Logger("tests/swr.ts");
*
* buildTestingContext() will return a testing context
*
+ * @deprecated do not use it, it will be removed
*/
export class SwrMockEnvironment extends MockEnvironment {
constructor(debug = false) {
diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts
index 4d7f3a8a1..9c820bb4b 100644
--- a/packages/web-util/src/utils/http-impl.sw.ts
+++ b/packages/web-util/src/utils/http-impl.sw.ts
@@ -18,11 +18,10 @@
* Imports.
*/
import {
+ Duration,
RequestThrottler,
- TalerErrorCode,
TalerError,
- Duration,
- CancellationToken,
+ TalerErrorCode
} from "@gnu-taler/taler-util";
import {
@@ -60,6 +59,7 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
const requestTimeout =
options?.timeout ?? Duration.fromMilliseconds(DEFAULT_REQUEST_TIMEOUT_MS);
const requestCancel = options?.cancellationToken;
+ const requestRedirect = options?.redirect;
const parsedUrl = new URL(requestUrl);
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
@@ -116,6 +116,7 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
body: myBody,
method: requestMethod,
signal: controller.signal,
+ redirect: requestRedirect
});
if (timeoutId) {
diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts
index 70f943540..23d3af468 100644
--- a/packages/web-util/src/utils/request.ts
+++ b/packages/web-util/src/utils/request.ts
@@ -17,6 +17,9 @@
import { HttpStatusCode } from "@gnu-taler/taler-util";
import { base64encode } from "./base64.js";
+/**
+ * @deprecated do not use it, it will be removed
+ */
export enum ErrorType {
CLIENT,
SERVER,
@@ -32,6 +35,7 @@ export enum ErrorType {
* @param baseUrl URL where the service is located
* @param endpoint endpoint of the service to be called
* @param options auth, method and params
+ * @deprecated do not use it, it will be removed
* @returns
*/
export async function defaultRequestHandler<T>(
@@ -189,16 +193,25 @@ export async function defaultRequestHandler<T>(
}
}
+/**
+ * @deprecated do not use it, it will be removed
+ */
export type HttpResponse<T, ErrorDetail> =
| HttpResponseOk<T>
| HttpResponseLoading<T>
| HttpError<ErrorDetail>;
+/**
+ * @deprecated do not use it, it will be removed
+ */
export type HttpResponsePaginated<T, ErrorDetail> =
| HttpResponseOkPaginated<T>
| HttpResponseLoading<T>
| HttpError<ErrorDetail>;
+/**
+ * @deprecated do not use it, it will be removed
+ */
export interface RequestInfo {
url: string;
hasToken: boolean;
@@ -215,6 +228,9 @@ interface HttpResponseLoading<T> {
data?: T;
}
+/**
+ * @deprecated do not use it, it will be removed
+ */
export interface HttpResponseOk<T> {
ok: true;
loading?: false;
@@ -225,8 +241,14 @@ export interface HttpResponseOk<T> {
info?: RequestInfo;
}
+/**
+ * @deprecated do not use it, it will be removed
+ */
export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination;
+/**
+ * @deprecated do not use it, it will be removed
+ */
export interface WithPagination {
loadMore: () => void;
loadMorePrev: () => void;
@@ -234,6 +256,9 @@ export interface WithPagination {
isReachingStart?: boolean;
}
+/**
+ * @deprecated do not use it, it will be removed
+ */
export type HttpError<ErrorDetail> =
| HttpRequestTimeoutError
| HttpResponseClientError<ErrorDetail>
@@ -241,6 +266,9 @@ export type HttpError<ErrorDetail> =
| HttpResponseUnreadableError
| HttpResponseUnexpectedError;
+/**
+ * @deprecated do not use it, it will be removed
+ */
export interface HttpResponseServerError<ErrorDetail> {
ok?: false;
loading?: false;
@@ -292,6 +320,9 @@ interface HttpResponseUnreadableError {
body: string;
message: string;
}
+/**
+ * @deprecated do not use it, it will be removed
+ */
export class RequestError<ErrorDetail> extends Error {
/**
* @deprecated use cause
@@ -307,6 +338,9 @@ export class RequestError<ErrorDetail> extends Error {
type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT";
+/**
+ * @deprecated do not use it, it will be removed
+ */
export interface RequestOptions {
method?: Methods;
token?: string;
@@ -323,6 +357,9 @@ export interface RequestOptions {
talerAmlOfficerSignature?: string;
}
+/**
+ * @deprecated do not use it, it will be removed
+ */
async function buildRequestOk<T>(
response: Response,
url: string,
@@ -345,6 +382,9 @@ async function buildRequestOk<T>(
};
}
+/**
+ * @deprecated do not use it, it will be removed
+ */
export function buildRequestFailed<ErrorDetail>(
url: string,
dataTxt: string,
@@ -424,6 +464,9 @@ export function buildRequestFailed<ErrorDetail>(
}
}
+/**
+ * @deprecated do not use it, it will be removed
+ */
function validateURL(baseUrl: string, endpoint: string): URL | undefined {
try {
return new URL(`${baseUrl}${endpoint}`)
diff --git a/packages/web-util/src/utils/route.ts b/packages/web-util/src/utils/route.ts
index 4f8a020f6..494a61efa 100644
--- a/packages/web-util/src/utils/route.ts
+++ b/packages/web-util/src/utils/route.ts
@@ -74,7 +74,7 @@ export function findMatch<T extends ObjectOf<RouteDefinition>>(
pagesMap: T,
pageList: Array<keyof T>,
path: string,
- params: Record<string, string>,
+ params: Record<string, string[]>,
): Location<T> | undefined {
for (let idx = 0; idx < pageList.length; idx++) {
const name = pageList[idx];
@@ -82,10 +82,6 @@ export function findMatch<T extends ObjectOf<RouteDefinition>>(
if (found !== null) {
const values = {} as Record<string, unknown>;
- Object.entries(params).forEach(([key, value]) => {
- values[key] = value;
- });
-
if (found.groups !== undefined) {
Object.entries(found.groups).forEach(([key, value]) => {
values[key] = value;
@@ -93,7 +89,7 @@ export function findMatch<T extends ObjectOf<RouteDefinition>>(
}
// @ts-expect-error values is a map string which is equivalent to the RouteParamsType
- return { name, parent: pagesMap, values };
+ return { name, parent: pagesMap, values, params };
}
}
return undefined;
@@ -117,6 +113,7 @@ type MapKeyValue<Type> = {
parent: Type;
name: Key;
values: RouteParamsType<Type, Key>;
+ params: Record<string, string[]>;
}
: never;
};