summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src')
-rw-r--r--packages/taler-wallet-webextension/src/NavigationBar.tsx335
-rw-r--r--packages/taler-wallet-webextension/src/background.dev.ts (renamed from packages/taler-wallet-webextension/src/hooks/useLang.ts)25
-rw-r--r--packages/taler-wallet-webextension/src/background.ts31
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js44
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map1
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts17
-rw-r--r--packages/taler-wallet-webextension/src/browserHttpLib.ts165
-rw-r--r--packages/taler-wallet-webextension/src/browserWorkerEntry.ts53
-rw-r--r--packages/taler-wallet-webextension/src/chromeBadge.ts16
-rw-r--r--packages/taler-wallet-webextension/src/compat.js61
-rw-r--r--packages/taler-wallet-webextension/src/compat.ts100
-rw-r--r--packages/taler-wallet-webextension/src/components/Amount.stories.tsx115
-rw-r--r--packages/taler-wallet-webextension/src/components/Amount.tsx107
-rw-r--r--packages/taler-wallet-webextension/src/components/AmountField.stories.tsx69
-rw-r--r--packages/taler-wallet-webextension/src/components/AmountField.tsx223
-rw-r--r--packages/taler-wallet-webextension/src/components/BalanceTable.tsx64
-rw-r--r--packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx360
-rw-r--r--packages/taler-wallet-webextension/src/components/Banner.stories.tsx126
-rw-r--r--packages/taler-wallet-webextension/src/components/Banner.tsx70
-rw-r--r--packages/taler-wallet-webextension/src/components/Checkbox.tsx48
-rw-r--r--packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx66
-rw-r--r--packages/taler-wallet-webextension/src/components/CopyButton.tsx54
-rw-r--r--packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx147
-rw-r--r--packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx47
-rw-r--r--packages/taler-wallet-webextension/src/components/Diagnostics.tsx93
-rw-r--r--packages/taler-wallet-webextension/src/components/EditableText.tsx79
-rw-r--r--packages/taler-wallet-webextension/src/components/EnabledBySettings.tsx38
-rw-r--r--packages/taler-wallet-webextension/src/components/ErrorMessage.tsx58
-rw-r--r--packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx73
-rw-r--r--packages/taler-wallet-webextension/src/components/ExchangeToS.tsx94
-rw-r--r--packages/taler-wallet-webextension/src/components/HistoryItem.tsx432
-rw-r--r--packages/taler-wallet-webextension/src/components/Loading.tsx100
-rw-r--r--packages/taler-wallet-webextension/src/components/LogoHeader.tsx34
-rw-r--r--packages/taler-wallet-webextension/src/components/Modal.tsx95
-rw-r--r--packages/taler-wallet-webextension/src/components/MultiActionButton.tsx129
-rw-r--r--packages/taler-wallet-webextension/src/components/Part.tsx192
-rw-r--r--packages/taler-wallet-webextension/src/components/PaymentButtons.tsx239
-rw-r--r--packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx118
-rw-r--r--packages/taler-wallet-webextension/src/components/PendingTransactions.tsx205
-rw-r--r--packages/taler-wallet-webextension/src/components/ProductList.tsx89
-rw-r--r--packages/taler-wallet-webextension/src/components/QR.stories.tsx (renamed from packages/taler-wallet-webextension/src/popup/Popup.stories.tsx)31
-rw-r--r--packages/taler-wallet-webextension/src/components/QR.tsx55
-rw-r--r--packages/taler-wallet-webextension/src/components/SelectList.tsx111
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx98
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx413
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/index.ts91
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/state.ts160
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx59
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/test.ts28
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts108
-rw-r--r--packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx225
-rw-r--r--packages/taler-wallet-webextension/src/components/Time.tsx46
-rw-r--r--packages/taler-wallet-webextension/src/components/TransactionItem.tsx190
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx1050
-rw-r--r--packages/taler-wallet-webextension/src/components/index.stories.tsx28
-rw-r--r--packages/taler-wallet-webextension/src/components/styled/index.tsx660
-rw-r--r--packages/taler-wallet-webextension/src/context/alert.ts277
-rw-r--r--packages/taler-wallet-webextension/src/context/backend.ts52
-rw-r--r--packages/taler-wallet-webextension/src/context/devContext.ts42
-rw-r--r--packages/taler-wallet-webextension/src/context/iocContext.ts67
-rw-r--r--packages/taler-wallet-webextension/src/context/translation.ts68
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/index.ts69
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/state.ts79
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx37
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/test.ts118
-rw-r--r--packages/taler-wallet-webextension/src/cta/Deposit/views.tsx72
-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.tsx33
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts65
-rw-r--r--packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx74
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts82
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts196
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx52
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/test.ts28
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx155
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts97
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts171
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx56
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/test.ts28
-rw-r--r--packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx60
-rw-r--r--packages/taler-wallet-webextension/src/cta/Pay.stories.tsx164
-rw-r--r--packages/taler-wallet-webextension/src/cta/Pay.tsx300
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/index.ts101
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/state.ts174
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/stories.tsx513
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/test.ts576
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx146
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts83
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts186
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx34
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts59
-rw-r--r--packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx83
-rw-r--r--packages/taler-wallet-webextension/src/cta/Recovery/index.ts65
-rw-r--r--packages/taler-wallet-webextension/src/cta/Recovery/state.ts84
-rw-r--r--packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx28
-rw-r--r--packages/taler-wallet-webextension/src/cta/Recovery/test.ts (renamed from packages/taler-wallet-webextension/src/permissions.ts)11
-rw-r--r--packages/taler-wallet-webextension/src/cta/Recovery/views.tsx41
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund.stories.tsx77
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund.tsx96
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/index.ts90
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/state.ts141
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/stories.tsx82
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/test.ts287
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund/views.tsx123
-rw-r--r--packages/taler-wallet-webextension/src/cta/Tip.stories.tsx59
-rw-r--r--packages/taler-wallet-webextension/src/cta/Tip.tsx111
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts70
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts185
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx50
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/test.ts28
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx125
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts75
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts99
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx47
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/test.ts28
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx70
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw.tsx383
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/index.ts139
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/state.ts522
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx327
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/test.ts296
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx333
-rw-r--r--packages/taler-wallet-webextension/src/cta/index.stories.ts29
-rw-r--r--packages/taler-wallet-webextension/src/cta/payback.tsx32
-rw-r--r--packages/taler-wallet-webextension/src/cta/reset-required.tsx97
-rw-r--r--packages/taler-wallet-webextension/src/cta/return-coins.tsx30
-rw-r--r--packages/taler-wallet-webextension/src/cta/termsExample.ts (renamed from packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx)467
-rw-r--r--packages/taler-wallet-webextension/src/custom.d.ts10
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts96
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts42
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts71
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useBalances.ts54
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts76
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts45
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts69
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useIsOnline.ts14
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts65
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts34
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts141
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useSettings.ts64
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts58
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts63
-rw-r--r--packages/taler-wallet-webextension/src/i18n/de.po2032
-rw-r--r--packages/taler-wallet-webextension/src/i18n/en-US.po294
-rw-r--r--packages/taler-wallet-webextension/src/i18n/es.po2197
-rw-r--r--packages/taler-wallet-webextension/src/i18n/fi.po1967
-rw-r--r--packages/taler-wallet-webextension/src/i18n/fr.po1911
-rw-r--r--packages/taler-wallet-webextension/src/i18n/it.po1911
-rw-r--r--packages/taler-wallet-webextension/src/i18n/ja.po1976
-rw-r--r--packages/taler-wallet-webextension/src/i18n/nl.po1953
-rw-r--r--packages/taler-wallet-webextension/src/i18n/poheader10
-rw-r--r--packages/taler-wallet-webextension/src/i18n/ru.po1977
-rw-r--r--packages/taler-wallet-webextension/src/i18n/strings-prelude10
-rw-r--r--packages/taler-wallet-webextension/src/i18n/strings.ts3448
-rw-r--r--packages/taler-wallet-webextension/src/i18n/sv.po2043
-rw-r--r--packages/taler-wallet-webextension/src/i18n/taler-wallet-webex.pot1900
-rw-r--r--packages/taler-wallet-webextension/src/i18n/tr.po2087
-rw-r--r--packages/taler-wallet-webextension/src/i18n/uk.po1956
-rw-r--r--packages/taler-wallet-webextension/src/mui/Alert.stories.tsx107
-rw-r--r--packages/taler-wallet-webextension/src/mui/Alert.tsx175
-rw-r--r--packages/taler-wallet-webextension/src/mui/Avatar.tsx69
-rw-r--r--packages/taler-wallet-webextension/src/mui/Button.stories.tsx163
-rw-r--r--packages/taler-wallet-webextension/src/mui/Button.tsx409
-rw-r--r--packages/taler-wallet-webextension/src/mui/Divider.tsx20
-rw-r--r--packages/taler-wallet-webextension/src/mui/Grid.stories.tsx212
-rw-r--r--packages/taler-wallet-webextension/src/mui/Grid.tsx347
-rw-r--r--packages/taler-wallet-webextension/src/mui/InputFile.tsx78
-rw-r--r--packages/taler-wallet-webextension/src/mui/Menu.stories.tsx171
-rw-r--r--packages/taler-wallet-webextension/src/mui/Menu.tsx135
-rw-r--r--packages/taler-wallet-webextension/src/mui/Modal.tsx152
-rw-r--r--packages/taler-wallet-webextension/src/mui/ModalManager.ts328
-rw-r--r--packages/taler-wallet-webextension/src/mui/Paper.stories.tsx148
-rw-r--r--packages/taler-wallet-webextension/src/mui/Paper.tsx85
-rw-r--r--packages/taler-wallet-webextension/src/mui/Popover.tsx71
-rw-r--r--packages/taler-wallet-webextension/src/mui/Portal.tsx128
-rw-r--r--packages/taler-wallet-webextension/src/mui/TextField.stories.tsx154
-rw-r--r--packages/taler-wallet-webextension/src/mui/TextField.tsx97
-rw-r--r--packages/taler-wallet-webextension/src/mui/Typography.tsx125
-rw-r--r--packages/taler-wallet-webextension/src/mui/colors/constants.ts342
-rw-r--r--packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts333
-rw-r--r--packages/taler-wallet-webextension/src/mui/colors/manipulation.ts328
-rw-r--r--packages/taler-wallet-webextension/src/mui/handlers.ts82
-rw-r--r--packages/taler-wallet-webextension/src/mui/index.stories.tsx27
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/FormControl.tsx176
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx70
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx85
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputBase.tsx562
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx199
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx114
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx142
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/SelectFilled.tsx20
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/SelectOutlined.tsx20
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/SelectStandard.tsx200
-rw-r--r--packages/taler-wallet-webextension/src/mui/style.tsx874
-rw-r--r--packages/taler-wallet-webextension/src/platform/api.ts337
-rw-r--r--packages/taler-wallet-webextension/src/platform/background.ts23
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts746
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts218
-rw-r--r--packages/taler-wallet-webextension/src/platform/firefox.ts92
-rw-r--r--packages/taler-wallet-webextension/src/platform/foreground.ts22
-rw-r--r--packages/taler-wallet-webextension/src/popup/Application.tsx229
-rw-r--r--packages/taler-wallet-webextension/src/popup/Backup.stories.tsx193
-rw-r--r--packages/taler-wallet-webextension/src/popup/BackupPage.tsx146
-rw-r--r--packages/taler-wallet-webextension/src/popup/Balance.stories.tsx386
-rw-r--r--packages/taler-wallet-webextension/src/popup/BalancePage.tsx294
-rw-r--r--packages/taler-wallet-webextension/src/popup/Debug.tsx64
-rw-r--r--packages/taler-wallet-webextension/src/popup/History.stories.tsx194
-rw-r--r--packages/taler-wallet-webextension/src/popup/History.tsx87
-rw-r--r--packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx53
-rw-r--r--packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx52
-rw-r--r--packages/taler-wallet-webextension/src/popup/ProviderAddPage.tsx244
-rw-r--r--packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx53
-rw-r--r--packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx238
-rw-r--r--packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx195
-rw-r--r--packages/taler-wallet-webextension/src/popup/Settings.tsx110
-rw-r--r--packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx39
-rw-r--r--packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx208
-rw-r--r--packages/taler-wallet-webextension/src/popup/index.stories.tsx23
-rw-r--r--packages/taler-wallet-webextension/src/popupEntryPoint.dev.tsx53
-rw-r--r--packages/taler-wallet-webextension/src/popupEntryPoint.tsx106
-rw-r--r--packages/taler-wallet-webextension/src/pwa/index.html114
-rw-r--r--packages/taler-wallet-webextension/src/pwa/manifest.json35
-rw-r--r--packages/taler-wallet-webextension/src/pwa/popup.html39
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/import.css35
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/roboto-italic-400.ttfbin0 -> 130872 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-300.tffbin0 -> 128256 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-400.ttfbin0 -> 129584 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-500.ttfbin0 -> 129768 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-700.ttfbin0 -> 128676 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-128.pngbin0 -> 8941 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-2022.svg468
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-48.pngbin0 -> 2790 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-512.pngbin0 -> 39994 bytes
-rw-r--r--packages/taler-wallet-webextension/src/pwa/stories.html12
-rw-r--r--packages/taler-wallet-webextension/src/pwa/sw.js6
-rw-r--r--packages/taler-wallet-webextension/src/pwa/tests.html23
-rw-r--r--packages/taler-wallet-webextension/src/pwa/wallet.html29
-rw-r--r--packages/taler-wallet-webextension/src/renderHtml.tsx180
-rw-r--r--packages/taler-wallet-webextension/src/stories.test.ts65
-rw-r--r--packages/taler-wallet-webextension/src/stories.tsx87
-rw-r--r--packages/taler-wallet-webextension/src/svg/check_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/chevron-down.inline.svg6
-rw-r--r--packages/taler-wallet-webextension/src/svg/close_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/svg/delete_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/svg/download_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/edit_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/error_outline_outlined_24px.inline.svg3
-rw-r--r--packages/taler-wallet-webextension/src/svg/index.tsx38
-rw-r--r--packages/taler-wallet-webextension/src/svg/info_outlined_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/svg/logo-2021.inline.svg9
-rw-r--r--packages/taler-wallet-webextension/src/svg/progress.inline.svg12
-rw-r--r--packages/taler-wallet-webextension/src/svg/qr_code_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/refresh_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/refresh_outlined_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/refresh_rounded_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/refresh_sharp_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/refresh_two_tone_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/report_problem_outlined_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/svg/ri-bank-line.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/ri-file-unknown-line.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/ri-hand-heart-line.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/ri-refresh-line.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/ri-refund-2-line.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/ri-shopping-cart-line.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/search_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/svg/send_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/svg/settings_black_24dp.inline.svg6
-rw-r--r--packages/taler-wallet-webextension/src/svg/spinner-bars.svg53
-rw-r--r--packages/taler-wallet-webextension/src/svg/success_outlined_24px.inline.svg4
-rw-r--r--packages/taler-wallet-webextension/src/svg/taler-logo-2021-plain.svg44
-rw-r--r--packages/taler-wallet-webextension/src/svg/upload_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/warning_24px.inline.svg1
-rw-r--r--packages/taler-wallet-webextension/src/svg/wifi.inline.svg3
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts372
-rw-r--r--packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts200
-rw-r--r--packages/taler-wallet-webextension/src/test-utils.ts202
-rw-r--r--packages/taler-wallet-webextension/src/utils/index.ts119
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts88
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts263
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx110
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts68
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/views.tsx158
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts92
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts198
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx27
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts209
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx251
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx (renamed from packages/taler-wallet-webextension/src/popup/Settings.stories.tsx)28
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx79
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Application.tsx677
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx316
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BackupPage.tsx374
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx106
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BalancePage.tsx126
-rw-r--r--packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx56
-rw-r--r--packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx57
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts121
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts276
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx116
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts431
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx191
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts98
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts198
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx65
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts153
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx430
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx50
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx695
-rw-r--r--packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/index.ts60
-rw-r--r--packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts24
-rw-r--r--packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/stories.tsx29
-rw-r--r--packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/test.ts28
-rw-r--r--packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/views.tsx25
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts115
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts242
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx563
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/test.ts23
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx931
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.stories.tsx672
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.tsx408
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts80
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts145
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx207
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/test.ts28
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx602
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx81
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/index.ts61
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/state.ts57
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx63
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/test.ts28
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx211
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx47
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx176
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx50
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx362
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx409
-rw-r--r--packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx29
-rw-r--r--packages/taler-wallet-webextension/src/wallet/QrReader.tsx392
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx40
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx41
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx80
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.tsx335
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx624
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx2157
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx36
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Welcome.tsx111
-rw-r--r--packages/taler-wallet-webextension/src/wallet/index.stories.tsx36
-rw-r--r--packages/taler-wallet-webextension/src/walletEntryPoint.dev.tsx53
-rw-r--r--packages/taler-wallet-webextension/src/walletEntryPoint.tsx95
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts513
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts818
352 files changed, 66465 insertions, 9706 deletions
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index 9edd8ca67..fe348f7fb 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -1,93 +1,304 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Popup shown to the user when they click
* the Taler browser action button.
*
- * @author Florian Dold
+ * @author sebasjm
*/
/**
* Imports.
*/
-import { i18n } from "@gnu-taler/taler-util";
-import { ComponentChildren, JSX, h } from "preact";
-import Match from "preact-router/match";
-import { useDevContext } from "./context/devContext";
-import { PopupNavigation } from './components/styled'
-
-export enum Pages {
- welcome = '/welcome',
- balance = '/balance',
- manual_withdraw = '/manual-withdraw',
- settings = '/settings',
- dev = '/dev',
- cta = '/cta',
- backup = '/backup',
- history = '/history',
- transaction = '/transaction/:tid',
- provider_detail = '/provider/:pid',
- provider_add = '/provider/add',
-
- reset_required = '/reset-required',
- payback = '/payback',
- return_coins = '/return-coins',
-
- pay = '/pay',
- refund = '/refund',
- tips = '/tip',
- withdraw = '/withdraw',
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { Fragment, h, VNode } from "preact";
+import { EnabledBySettings } from "./components/EnabledBySettings.js";
+import {
+ NavigationHeader,
+ NavigationHeaderHolder,
+ SvgIcon,
+} from "./components/styled/index.js";
+import { 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";
+import { parseTalerUri, TalerUriAction } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+
+/**
+ * List of pages used by the wallet
+ *
+ * @author sebasjm
+ */
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+type PageLocation<DynamicPart extends object> = {
+ 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] ? "" : encodeURIComponent(values[v]),
+ );
+ }
+ return result;
}
-interface TabProps {
- target: string;
- current?: string;
- children?: ComponentChildren;
+// eslint-disable-next-line @typescript-eslint/ban-types
+function pageDefinition<T extends object>(pattern: string): PageLocation<T> {
+ const patternParams = pattern.match(/(:[\w?]*)/g);
+ if (!patternParams)
+ 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 ?? {}) as Record<string, string>);
+ f.pattern = pattern;
+ return f;
}
-function Tab(props: TabProps): JSX.Element {
- let cssClass = "";
- if (props.current?.startsWith(props.target)) {
- cssClass = "active";
+export const Pages = {
+ welcome: "/welcome",
+ balance: "/balance",
+ balanceHistory: pageDefinition<{ currency?: string }>(
+ "/balance/history/:currency?",
+ ),
+ searchHistory: pageDefinition<{ currency?: string }>(
+ "/search/history/:currency?",
+ ),
+ balanceDeposit: pageDefinition<{ amount: string }>(
+ "/balance/deposit/:amount",
+ ),
+ balanceTransaction: pageDefinition<{ tid: string }>(
+ "/balance/transaction/:tid",
+ ),
+ sendCash: pageDefinition<{ amount?: string }>("/destination/send/:amount"),
+ receiveCash: pageDefinition<{ amount?: string }>("/destination/get/:amount?"),
+ dev: "/dev",
+
+ exchanges: "/exchanges",
+ backup: "/backup",
+ backupProviderDetail: pageDefinition<{ pid: string }>(
+ "/backup/provider/:pid",
+ ),
+ backupProviderAdd: "/backup/provider/add",
+
+ qr: "/qr",
+ notifications: "/notifications",
+ settings: "/settings",
+ settingsExchangeAdd: pageDefinition<{ currency?: string }>(
+ "/settings/exchange/add/:currency?",
+ ),
+
+ defaultCta: pageDefinition<{ uri: string }>("/taler-uri/:uri"),
+ cta: pageDefinition<{ action: string }>("/cta/:action"),
+ ctaPay: "/cta/pay",
+ ctaPayTemplate: "/cta/pay/template",
+ ctaRecovery: "/cta/recovery",
+ ctaRefund: "/cta/refund",
+ ctaWithdraw: "/cta/withdraw",
+ ctaDeposit: "/cta/deposit",
+ ctaExperiment: "/cta/experiment",
+ ctaAddExchange: "/cta/add/exchange",
+ ctaInvoiceCreate: pageDefinition<{ amount?: string }>(
+ "/cta/invoice/create/:amount?",
+ ),
+ ctaTransferCreate: pageDefinition<{ amount?: string }>(
+ "/cta/transfer/create/:amount?",
+ ),
+ ctaInvoicePay: "/cta/invoice/pay",
+ ctaTransferPickup: "/cta/transfer/pickup",
+ ctaWithdrawManual: pageDefinition<{ amount?: string }>(
+ "/cta/manual-withdraw/:amount?",
+ ),
+};
+
+const talerUriActionToPageName: {
+ [t in TalerUriAction]: keyof typeof Pages | undefined;
+} = {
+ [TalerUriAction.Withdraw]: "ctaWithdraw",
+ [TalerUriAction.Pay]: "ctaPay",
+ [TalerUriAction.Refund]: "ctaRefund",
+ [TalerUriAction.PayPull]: "ctaInvoicePay",
+ [TalerUriAction.PayPush]: "ctaTransferPickup",
+ [TalerUriAction.Restore]: "ctaRecovery",
+ [TalerUriAction.PayTemplate]: "ctaPayTemplate",
+ [TalerUriAction.WithdrawExchange]: "ctaWithdrawManual",
+ [TalerUriAction.DevExperiment]: "ctaExperiment",
+ [TalerUriAction.AddExchange]: "ctaAddExchange",
+};
+
+export function getPathnameForTalerURI(talerUri: string): string | undefined {
+ const uri = parseTalerUri(talerUri);
+ if (!uri) {
+ return undefined;
}
+ const pageName = talerUriActionToPageName[uri.type];
+ if (!pageName) {
+ return undefined;
+ }
+ const pageString: string =
+ typeof Pages[pageName] === "function"
+ ? (Pages[pageName] as any)()
+ : Pages[pageName];
+ return `${pageString}?talerUri=${encodeURIComponent(talerUri)}`;
+}
+
+export type PopupNavBarOptions = "balance" | "backup" | "dev";
+export function PopupNavBar({ path }: { path?: PopupNavBarOptions }): VNode {
+ const api = useBackendContext();
+ const hook = useAsyncAsHook(async () => {
+ return await api.wallet.call(
+ WalletApiOperation.GetUserAttentionUnreadCount,
+ {},
+ );
+ });
+ const attentionCount = !hook || hook.hasError ? 0 : hook.response.total;
+
+ const { i18n } = useTranslationContext();
return (
- <a href={props.target} class={cssClass}>
- {props.children}
- </a>
+ <NavigationHeader>
+ <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
+ <i18n.Translate>Balance</i18n.Translate>
+ </a>
+ <EnabledBySettings name="backup">
+ <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
+ <i18n.Translate>Backup</i18n.Translate>
+ </a>
+ </EnabledBySettings>
+ <div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
+ {attentionCount > 0 ? (
+ <a href={Pages.notifications}>
+ <SvgIcon
+ title={i18n.str`Notifications`}
+ dangerouslySetInnerHTML={{ __html: warningIcon }}
+ color="yellow"
+ />
+ </a>
+ ) : (
+ <Fragment />
+ )}
+ <a href={Pages.qr}>
+ <SvgIcon
+ title={i18n.str`QR Reader and Taler URI`}
+ dangerouslySetInnerHTML={{ __html: qrIcon }}
+ color="white"
+ />
+ </a>
+ <a href={Pages.settings}>
+ <SvgIcon
+ title={i18n.str`Settings`}
+ dangerouslySetInnerHTML={{ __html: settingsIcon }}
+ color="white"
+ />
+ </a>
+ </div>
+ </NavigationHeader>
);
}
+export type WalletNavBarOptions = "balance" | "backup" | "dev";
+export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
+ const { i18n } = useTranslationContext();
-export function NavBar({ devMode, path }: { path: string, devMode: boolean }) {
- return <PopupNavigation devMode={devMode}>
- <div>
- <Tab target="/balance" current={path}>{i18n.str`Balance`}</Tab>
- <Tab target="/history" current={path}>{i18n.str`History`}</Tab>
- <Tab target="/backup" current={path}>{i18n.str`Backup`}</Tab>
- <Tab target="/settings" current={path}>{i18n.str`Settings`}</Tab>
- {devMode && <Tab target="/dev" current={path}>{i18n.str`Dev`}</Tab>}
- </div>
- </PopupNavigation>
-}
+ const api = useBackendContext();
+ const hook = useAsyncAsHook(async () => {
+ return await api.wallet.call(
+ WalletApiOperation.GetUserAttentionUnreadCount,
+ {},
+ );
+ });
+ const attentionCount =
+ (!hook || hook.hasError ? 0 : hook.response?.total) ?? 0;
-export function WalletNavBar() {
- const { devMode } = useDevContext()
- return <Match>{({ path }: any) => {
- console.log("path", path)
- return <NavBar devMode={devMode} path={path} />
- }}</Match>
-}
+ return (
+ <NavigationHeaderHolder>
+ <NavigationHeader>
+ <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
+ <i18n.Translate>Balance</i18n.Translate>
+ </a>
+ <EnabledBySettings name="backup">
+ <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
+ <i18n.Translate>Backup</i18n.Translate>
+ </a>
+ </EnabledBySettings>
+
+ {attentionCount > 0 ? (
+ <a href={Pages.notifications}>
+ <i18n.Translate>Notifications</i18n.Translate>
+ </a>
+ ) : (
+ <Fragment />
+ )}
+ <EnabledBySettings name="advancedMode">
+ <a href={Pages.dev} class={path === "dev" ? "active" : ""}>
+ <i18n.Translate>Dev tools</i18n.Translate>
+ </a>
+ </EnabledBySettings>
+
+ <div
+ style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}
+ >
+ <a href={Pages.searchHistory({})}>
+ <SvgIcon
+ title={i18n.str`Search transactions`}
+ dangerouslySetInnerHTML={{ __html: searchIcon }}
+ color="white"
+ />
+ </a>
+ <a href={Pages.qr}>
+ <SvgIcon
+ title={i18n.str`QR Reader and Taler URI`}
+ dangerouslySetInnerHTML={{ __html: qrIcon }}
+ color="white"
+ />
+ </a>
+ <a href={Pages.settings}>
+ <SvgIcon
+ title={i18n.str`Settings`}
+ dangerouslySetInnerHTML={{ __html: settingsIcon }}
+ color="white"
+ />
+ </a>
+ </div>
+ </NavigationHeader>
+ </NavigationHeaderHolder>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useLang.ts b/packages/taler-wallet-webextension/src/background.dev.ts
index 70b9614f6..96cf63409 100644
--- a/packages/taler-wallet-webextension/src/hooks/useLang.ts
+++ b/packages/taler-wallet-webextension/src/background.dev.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,10 +14,23 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { useNotNullLocalStorage } from './useLocalStorage';
+/**
+ * Entry point for the background page.
+ *
+ * @author sebasjm
+ */
+
+/**
+ * Imports.
+ */
+import { platform, setupPlatform } from "./platform/background.js";
+import devAPI from "./platform/dev.js";
+import { wxMain } from "./wxBackend.js";
+
+setupPlatform(devAPI);
-export function useLang(initial?: string): [string, (s:string) => void] {
- const browserLang: string | undefined = typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : undefined;
- const defaultLang = (browserLang || initial || 'en').substring(0, 2)
- return useNotNullLocalStorage('lang-preference', defaultLang)
+async function start() {
+ await platform.notifyWhenAppIsReady();
+ await wxMain();
}
+start();
diff --git a/packages/taler-wallet-webextension/src/background.ts b/packages/taler-wallet-webextension/src/background.ts
index dcbf96139..7df66eff8 100644
--- a/packages/taler-wallet-webextension/src/background.ts
+++ b/packages/taler-wallet-webextension/src/background.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -17,14 +17,33 @@
/**
* Entry point for the background page.
*
- * @author Florian Dold
+ * @author sebasjm
*/
/**
* Imports.
*/
-import { wxMain } from "./wxBackend";
+import { platform, setupPlatform } from "./platform/background.js";
+import chromeAPI from "./platform/chrome.js";
+import firefoxAPI from "./platform/firefox.js";
+import { wxMain } from "./wxBackend.js";
-window.addEventListener("load", () => {
- wxMain();
-});
+const isFirefox =
+ typeof (window as any) !== "undefined" &&
+ typeof (window as any)["InstallTrigger"] !== "undefined";
+
+// FIXME: create different entry point for any platform instead of
+// switching in runtime
+if (isFirefox) {
+ setupPlatform(firefoxAPI);
+} else {
+ setupPlatform(chromeAPI);
+}
+
+// setGlobalLogLevelFromString("trace")
+
+async function start() {
+ await platform.notifyWhenAppIsReady();
+ await wxMain();
+}
+start();
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js
deleted file mode 100644
index e9492a2fb..000000000
--- a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js
+++ /dev/null
@@ -1,44 +0,0 @@
-"use strict";
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.BrowserCryptoWorkerFactory = void 0;
-/**
- * API to access the Taler crypto worker thread.
- * @author Florian Dold
- */
-class BrowserCryptoWorkerFactory {
- startWorker() {
- const workerCtor = Worker;
- const workerPath = "/browserWorkerEntry.js";
- return new workerCtor(workerPath);
- }
- getConcurrency() {
- let concurrency = 2;
- try {
- // only works in the browser
- // tslint:disable-next-line:no-string-literal
- concurrency = navigator["hardwareConcurrency"];
- concurrency = Math.max(1, Math.ceil(concurrency / 2));
- }
- catch (e) {
- concurrency = 2;
- }
- return concurrency;
- }
-}
-exports.BrowserCryptoWorkerFactory = BrowserCryptoWorkerFactory;
-//# sourceMappingURL=browserCryptoWorkerFactory.js.map \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map
deleted file mode 100644
index db56d4451..000000000
--- a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"browserCryptoWorkerFactory.js","sourceRoot":"","sources":["browserCryptoWorkerFactory.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAEH;;;GAGG;AAEH,MAAa,0BAA0B;IACrC,WAAW;QACT,MAAM,UAAU,GAAG,MAAM,CAAC;QAC1B,MAAM,UAAU,GAAG,wBAAwB,CAAC;QAC5C,OAAO,IAAI,UAAU,CAAC,UAAU,CAAiB,CAAC;IACpD,CAAC;IAED,cAAc;QACZ,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,IAAI;YACF,4BAA4B;YAC5B,6CAA6C;YAC7C,WAAW,GAAI,SAAiB,CAAC,qBAAqB,CAAC,CAAC;YACxD,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC;SACvD;QAAC,OAAO,CAAC,EAAE;YACV,WAAW,GAAG,CAAC,CAAC;SACjB;QACD,OAAO,WAAW,CAAC;IACrB,CAAC;CACF;AAnBD,gEAmBC"} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts
index a8315dc6d..c93097da8 100644
--- a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts
+++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts
@@ -1,17 +1,17 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
@@ -19,12 +19,17 @@
* @author Florian Dold
*/
-import type { CryptoWorker, CryptoWorkerFactory } from "@gnu-taler/taler-wallet-core";
+import type {
+ CryptoWorker,
+ CryptoWorkerFactory,
+} from "@gnu-taler/taler-wallet-core";
export class BrowserCryptoWorkerFactory implements CryptoWorkerFactory {
startWorker(): CryptoWorker {
const workerCtor = Worker;
const workerPath = "/dist/browserWorkerEntry.js";
+ // FIXME: This is not really the same interface as the crypto worker!
+ // We need to wrap it.
return new workerCtor(workerPath) as CryptoWorker;
}
diff --git a/packages/taler-wallet-webextension/src/browserHttpLib.ts b/packages/taler-wallet-webextension/src/browserHttpLib.ts
deleted file mode 100644
index 63fd456f4..000000000
--- a/packages/taler-wallet-webextension/src/browserHttpLib.ts
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
- OperationFailedError,
- HttpRequestLibrary,
- HttpRequestOptions,
- HttpResponse,
- Headers,
-} from "@gnu-taler/taler-wallet-core";
-import { Logger, TalerErrorCode } from "@gnu-taler/taler-util";
-
-const logger = new Logger("browserHttpLib");
-
-/**
- * An implementation of the [[HttpRequestLibrary]] using the
- * browser's XMLHttpRequest.
- */
-export class BrowserHttpLib implements HttpRequestLibrary {
- fetch(url: string, options?: HttpRequestOptions): Promise<HttpResponse> {
- const method = options?.method ?? "GET";
- let requestBody = options?.body;
- return new Promise<HttpResponse>((resolve, reject) => {
- const myRequest = new XMLHttpRequest();
- myRequest.open(method, url);
- if (options?.headers) {
- for (const headerName in options.headers) {
- myRequest.setRequestHeader(headerName, options.headers[headerName]);
- }
- }
- myRequest.responseType = "arraybuffer";
- if (requestBody) {
- myRequest.send(requestBody);
- } else {
- myRequest.send();
- }
-
- myRequest.onerror = (e) => {
- logger.error("http request error");
- reject(
- OperationFailedError.fromCode(
- TalerErrorCode.WALLET_NETWORK_ERROR,
- "Could not make request",
- {
- requestUrl: url,
- },
- ),
- );
- };
-
- myRequest.addEventListener("readystatechange", (e) => {
- if (myRequest.readyState === XMLHttpRequest.DONE) {
- if (myRequest.status === 0) {
- const exc = OperationFailedError.fromCode(
- TalerErrorCode.WALLET_NETWORK_ERROR,
- "HTTP request failed (status 0, maybe URI scheme was wrong?)",
- {
- requestUrl: url,
- },
- );
- reject(exc);
- return;
- }
- const makeText = async (): Promise<string> => {
- const td = new TextDecoder();
- return td.decode(myRequest.response);
- };
- const makeJson = async (): Promise<any> => {
- let responseJson;
- try {
- const td = new TextDecoder();
- const responseString = td.decode(myRequest.response);
- responseJson = JSON.parse(responseString);
- } catch (e) {
- throw OperationFailedError.fromCode(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- "Invalid JSON from HTTP response",
- {
- requestUrl: url,
- httpStatusCode: myRequest.status,
- },
- );
- }
- if (responseJson === null || typeof responseJson !== "object") {
- throw OperationFailedError.fromCode(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- "Invalid JSON from HTTP response",
- {
- requestUrl: url,
- httpStatusCode: myRequest.status,
- },
- );
- }
- return responseJson;
- };
-
- const headers = myRequest.getAllResponseHeaders();
- const arr = headers.trim().split(/[\r\n]+/);
-
- // Create a map of header names to values
- const headerMap: Headers = new Headers();
- arr.forEach(function (line) {
- const parts = line.split(": ");
- const headerName = parts.shift();
- if (!headerName) {
- logger.warn("skipping invalid header");
- return;
- }
- const value = parts.join(": ");
- headerMap.set(headerName, value);
- });
- const resp: HttpResponse = {
- requestUrl: url,
- status: myRequest.status,
- headers: headerMap,
- requestMethod: method,
- json: makeJson,
- text: makeText,
- bytes: async () => myRequest.response,
- };
- resolve(resp);
- }
- });
- });
- }
-
- get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
- return this.fetch(url, {
- method: "GET",
- ...opt,
- });
- }
-
- postJson(
- url: string,
- body: any,
- opt?: HttpRequestOptions,
- ): Promise<HttpResponse> {
- return this.fetch(url, {
- method: "POST",
- body: JSON.stringify(body),
- ...opt,
- });
- }
-
- stop(): void {
- // Nothing to do
- }
-}
diff --git a/packages/taler-wallet-webextension/src/browserWorkerEntry.ts b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
index b5c26a7bb..bb1794e56 100644
--- a/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
+++ b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
@@ -1,18 +1,18 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
/**
* Web worker for crypto operations.
@@ -22,38 +22,51 @@
* Imports.
*/
-import { Logger } from "@gnu-taler/taler-util";
-import { CryptoImplementation } from "@gnu-taler/taler-wallet-core";
+import {
+ j2s,
+ Logger,
+ getErrorDetailFromException,
+} from "@gnu-taler/taler-util";
+import { nativeCrypto } from "@gnu-taler/taler-wallet-core";
const logger = new Logger("browserWorkerEntry.ts");
-const worker: Worker = (self as any) as Worker;
+const worker: Worker = self as any as Worker;
async function handleRequest(
operation: string,
id: number,
- args: string[],
+ req: unknown,
): Promise<void> {
- const impl = new CryptoImplementation();
+ const impl = nativeCrypto;
if (!(operation in impl)) {
console.error(`crypto operation '${operation}' not found`);
return;
}
+ logger.info(`browser worker crypto request: ${j2s(req)}`);
+
+ let responseMsg: any;
try {
- const result = (impl as any)[operation](...args);
- worker.postMessage({ result, id });
- } catch (e) {
- logger.error("error during operation", e);
- return;
+ const result = await (impl as any)[operation](impl, req);
+ responseMsg = { type: "success", result, id };
+ } catch (e: any) {
+ logger.error(`error during operation: ${e.stack ?? e.toString()}`);
+ responseMsg = {
+ type: "error",
+ id,
+ error: getErrorDetailFromException(e),
+ };
}
+
+ worker.postMessage(responseMsg);
}
worker.onmessage = (msg: MessageEvent) => {
- const args = msg.data.args;
- if (!Array.isArray(args)) {
- console.error("args must be array");
+ const req = msg.data.req;
+ if (typeof req !== "object") {
+ console.error("request must be an object");
return;
}
const id = msg.data.id;
@@ -67,7 +80,7 @@ worker.onmessage = (msg: MessageEvent) => {
return;
}
- handleRequest(operation, id, args).catch((e) => {
+ handleRequest(operation, id, req).catch((e) => {
console.error("error in browser worker", e);
});
};
diff --git a/packages/taler-wallet-webextension/src/chromeBadge.ts b/packages/taler-wallet-webextension/src/chromeBadge.ts
index 7bc5d368d..63d0372b6 100644
--- a/packages/taler-wallet-webextension/src/chromeBadge.ts
+++ b/packages/taler-wallet-webextension/src/chromeBadge.ts
@@ -1,20 +1,20 @@
/*
- This file is part of TALER
- (C) 2016 INRIA
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { isFirefox } from "./compat";
+import { platform } from "./platform/background.js";
/**
* Polyfill for requestAnimationFrame, which
@@ -198,7 +198,7 @@ export class ChromeBadge {
this.canvas.width,
this.canvas.height,
);
- chrome.browserAction.setIcon({ imageData });
+ chrome.action.setIcon({ imageData });
} catch (e) {
// Might fail if browser has over-eager canvas fingerprinting countermeasures.
// There's nothing we can do then ...
@@ -210,7 +210,7 @@ export class ChromeBadge {
if (this.animationRunning) {
return;
}
- if (isFirefox()) {
+ if (platform.isFirefox()) {
// Firefox does not support badge animations properly
return;
}
diff --git a/packages/taler-wallet-webextension/src/compat.js b/packages/taler-wallet-webextension/src/compat.js
deleted file mode 100644
index fdfcbd4b9..000000000
--- a/packages/taler-wallet-webextension/src/compat.js
+++ /dev/null
@@ -1,61 +0,0 @@
-"use strict";
-/*
- This file is part of TALER
- (C) 2017 INRIA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.getPermissionsApi = exports.isNode = exports.isFirefox = void 0;
-/**
- * Compatibility helpers needed for browsers that don't implement
- * WebExtension APIs consistently.
- */
-function isFirefox() {
- const rt = chrome.runtime;
- if (typeof rt.getBrowserInfo === "function") {
- return true;
- }
- return false;
-}
-exports.isFirefox = isFirefox;
-/**
- * Check if we are running under nodejs.
- */
-function isNode() {
- return typeof process !== "undefined" && process.release.name === "node";
-}
-exports.isNode = isNode;
-function getPermissionsApi() {
- const myBrowser = globalThis.browser;
- if (typeof myBrowser === "object" &&
- typeof myBrowser.permissions === "object") {
- return {
- addPermissionsListener: () => {
- // Not supported yet.
- },
- contains: myBrowser.permissions.contains,
- request: myBrowser.permissions.request,
- remove: myBrowser.permissions.remove,
- };
- }
- else {
- return {
- addPermissionsListener: chrome.permissions.onAdded.addListener.bind(chrome.permissions.onAdded),
- contains: chrome.permissions.contains,
- request: chrome.permissions.request,
- remove: chrome.permissions.remove,
- };
- }
-}
-exports.getPermissionsApi = getPermissionsApi;
-//# sourceMappingURL=compat.js.map \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/compat.ts b/packages/taler-wallet-webextension/src/compat.ts
deleted file mode 100644
index 36846e615..000000000
--- a/packages/taler-wallet-webextension/src/compat.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- This file is part of TALER
- (C) 2017 INRIA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Compatibility helpers needed for browsers that don't implement
- * WebExtension APIs consistently.
- */
-
-// globalThis polyfill, see https://mathiasbynens.be/notes/globalthis
-(function () {
- if (typeof globalThis === "object") return;
- Object.defineProperty(Object.prototype, "__magic__", {
- get: function () {
- return this;
- },
- configurable: true, // This makes it possible to `delete` the getter later.
- });
- // @ts-ignore: polyfill magic
- __magic__.globalThis = __magic__; // lolwat
- // @ts-ignore: polyfill magic
- delete Object.prototype.__magic__;
-})();
-
-export function isFirefox(): boolean {
- const rt = chrome.runtime as any;
- if (typeof rt.getBrowserInfo === "function") {
- return true;
- }
- return false;
-}
-
-/**
- * Check if we are running under nodejs.
- */
-export function isNode(): boolean {
- return typeof process !== "undefined" && process.release.name === "node";
-}
-
-/**
- * Compatibility API that works on multiple browsers.
- */
-export interface CrossBrowserPermissionsApi {
- contains(
- permissions: chrome.permissions.Permissions,
- callback: (result: boolean) => void,
- ): void;
-
- addPermissionsListener(
- callback: (permissions: chrome.permissions.Permissions) => void,
- ): void;
-
- request(
- permissions: chrome.permissions.Permissions,
- callback?: (granted: boolean) => void,
- ): void;
-
- remove(
- permissions: chrome.permissions.Permissions,
- callback?: (removed: boolean) => void,
- ): void;
-}
-
-export function getPermissionsApi(): CrossBrowserPermissionsApi {
- const myBrowser = (globalThis as any).browser;
- if (
- typeof myBrowser === "object" &&
- typeof myBrowser.permissions === "object"
- ) {
- return {
- addPermissionsListener: () => {
- // Not supported yet.
- },
- contains: myBrowser.permissions.contains,
- request: myBrowser.permissions.request,
- remove: myBrowser.permissions.remove,
- };
- } else {
- return {
- addPermissionsListener: chrome.permissions.onAdded.addListener.bind(
- chrome.permissions.onAdded,
- ),
- contains: chrome.permissions.contains,
- request: chrome.permissions.request,
- remove: chrome.permissions.remove,
- };
- }
-}
diff --git a/packages/taler-wallet-webextension/src/components/Amount.stories.tsx b/packages/taler-wallet-webextension/src/components/Amount.stories.tsx
new file mode 100644
index 000000000..fa28088eb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Amount.stories.tsx
@@ -0,0 +1,115 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { styled } from "@linaria/react";
+import { Fragment, h, VNode } from "preact";
+import { Amount } from "./Amount.js";
+import { AmountString } from "@gnu-taler/taler-util";
+
+export default {
+ title: "amount",
+ component: Amount,
+};
+
+const Table = styled.table`
+ td {
+ padding: 4px;
+ }
+ td {
+ border-bottom: 1px solid black;
+ }
+`;
+
+function ProductTable(
+ prods: string[],
+ AmountRender: (p: { value: AmountString; index: number }) => VNode = Amount,
+): VNode {
+ return (
+ <Table>
+ <tr>
+ <td>product</td>
+ <td>price</td>
+ </tr>
+ {prods.map((value, i) => {
+ return (
+ <tr key={i}>
+ <td>p{i}</td>
+ <td>
+ <AmountRender value={value as AmountString} index={i} />
+ {/* <Amount value={value} fracSize={fracSize} /> */}
+ </td>
+ </tr>
+ );
+ })}
+ </Table>
+ );
+}
+
+export const WithoutFixedSizeDefault = (): VNode =>
+ ProductTable(["ARS:19", "ARS:0.1", "ARS:10.02"]);
+
+export const WithFixedSizeZero = (): VNode =>
+ ProductTable(["ARS:19", "ARS:0.1", "ARS:10.02"], ({ value }) => {
+ return <Amount value={value} maxFracSize={0} />;
+ });
+
+export const WithFixedSizeFour = (): VNode =>
+ ProductTable(
+ ["ARS:19", "ARS:0.1", "ARS:10.02", "ARS:10.0123", "ARS:10.0123123"],
+ ({ value }) => {
+ return <Amount value={value} maxFracSize={4} />;
+ },
+ );
+
+export const WithFixedSizeFourNegative = (): VNode =>
+ ProductTable(
+ ["ARS:19", "ARS:0.1", "ARS:10.02", "ARS:10.0123", "ARS:10.0123123"],
+ ({ value, index }) => {
+ return (
+ <Amount value={value} maxFracSize={4} negative={index % 2 === 0} />
+ );
+ },
+ );
+
+export const WithFixedSizeFourOverflow = (): VNode =>
+ ProductTable(
+ ["ARS:19", "ARS:0.1", "ARS:10123123.02", "ARS:10.0123", "ARS:10.0123123"],
+ ({ value, index }) => {
+ return (
+ <Amount value={value} maxFracSize={4} negative={index % 2 === 0} />
+ );
+ },
+ );
+
+export const WithFixedSizeFourAccounting = (): VNode =>
+ ProductTable(
+ ["ARS:19", "ARS:0.1", "ARS:10123123.02", "ARS:10.0123", "ARS:10.0123123"],
+ ({ value, index }) => {
+ return (
+ <Amount
+ value={value}
+ signType="accounting"
+ maxFracSize={4}
+ negative={index % 2 === 0}
+ />
+ );
+ },
+ );
diff --git a/packages/taler-wallet-webextension/src/components/Amount.tsx b/packages/taler-wallet-webextension/src/components/Amount.tsx
new file mode 100644
index 000000000..09f65473c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Amount.tsx
@@ -0,0 +1,107 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ amountFractionalBase,
+ amountFractionalLength,
+ AmountJson,
+ Amounts,
+ AmountString,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+
+export function Amount({
+ value,
+ maxFracSize,
+ negative,
+ hideCurrency,
+ signType = "standard",
+ signDisplay = "auto",
+}: {
+ negative?: boolean;
+ value: AmountJson | AmountString;
+ maxFracSize?: number;
+ hideCurrency?: boolean;
+ signType?: "accounting" | "standard";
+ signDisplay?: "auto" | "always" | "never" | "exceptZero";
+}): VNode {
+ const aj = Amounts.jsonifyAmount(value);
+ const minFractional =
+ maxFracSize !== undefined && maxFracSize < 2 ? maxFracSize : 2;
+ const af = aj.fraction % amountFractionalBase;
+ let s = "";
+ if ((af && maxFracSize) || minFractional > 0) {
+ s += ".";
+ let n = af;
+ for (
+ let i = 0;
+ (maxFracSize === undefined || i < maxFracSize) &&
+ i < amountFractionalLength;
+ i++
+ ) {
+ if (!n && i >= minFractional) {
+ break;
+ }
+ s = s + Math.floor((n / amountFractionalBase) * 10).toString();
+ n = (n * 10) % amountFractionalBase;
+ }
+ }
+ const fontSize = 18;
+ const letterSpacing = 0;
+ const mult = 0.7;
+ return (
+ <span style={{ textAlign: "right", whiteSpace: "nowrap" }}>
+ <span
+ style={{
+ display: "inline-block",
+ fontFamily: "monospace",
+ fontSize,
+ }}
+ >
+ {negative ? (signType === "accounting" ? "(" : "-") : ""}
+ <span
+ style={{
+ display: "inline-block",
+ textAlign: "right",
+ fontFamily: "monospace",
+ fontSize,
+ letterSpacing,
+ }}
+ >
+ {aj.value}
+ </span>
+ <span
+ style={{
+ display: "inline-block",
+ width: !maxFracSize ? undefined : `${(maxFracSize + 1) * mult}em`,
+ textAlign: "left",
+ fontFamily: "monospace",
+ fontSize,
+ letterSpacing,
+ }}
+ >
+ {s}
+ {negative && signType === "accounting" ? ")" : ""}
+ </span>
+ </span>
+ {hideCurrency ? undefined : (
+ <Fragment>
+ &nbsp;
+ <span>{aj.currency}</span>
+ </Fragment>
+ )}
+ </span>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
new file mode 100644
index 000000000..daa06fa65
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/AmountField.stories.tsx
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AmountJson, Amounts } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { AmountFieldHandler, nullFunction, withSafe } from "../mui/handlers.js";
+import { AmountField } from "./AmountField.js";
+
+export default {
+ title: "amountField",
+};
+
+function RenderAmount(): VNode {
+ const [value, setValue] = useState<AmountJson | undefined>({
+ currency: "USD",
+ value: 1,
+ fraction: 0,
+ });
+
+ const error = value === undefined ? undefined : undefined;
+
+ const handler: AmountFieldHandler = {
+ value: value ?? Amounts.zeroOfCurrency("USD"),
+ onInput: withSafe(async (e) => {
+ setValue(e);
+ }, nullFunction),
+ error,
+ };
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <AmountField
+ required
+ label={i18n.str`Amount`}
+ highestDenom={2000000}
+ lowestDenom={0.01}
+ handler={handler}
+ />
+ <p>
+ <pre>
+ value : {value?.value} <br />
+ fraction : {value?.fraction}
+ </pre>
+ </p>
+ </Fragment>
+ );
+}
+
+export const AmountFieldExample = (): VNode => RenderAmount();
diff --git a/packages/taler-wallet-webextension/src/components/AmountField.tsx b/packages/taler-wallet-webextension/src/components/AmountField.tsx
new file mode 100644
index 000000000..c330c72b5
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/AmountField.tsx
@@ -0,0 +1,223 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ amountFractionalBase,
+ amountFractionalLength,
+ AmountJson,
+ amountMaxValue,
+ Amounts,
+ Result,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { AmountFieldHandler } from "../mui/handlers.js";
+import { TextField } from "../mui/TextField.js";
+
+const HIGH_DENOM_SYMBOL = ["", "K", "M", "G", "T", "P"];
+const LOW_DENOM_SYMBOL = ["", "m", "mm", "n", "p", "f"];
+
+/**
+ * Show normalized value based on the currency unit
+ */
+export function AmountField({
+ label,
+ handler,
+ lowestDenom = 1,
+ highestDenom = 1,
+ required,
+}: {
+ label: TranslatedString;
+ lowestDenom?: number;
+ highestDenom?: number;
+ required?: boolean;
+ handler: AmountFieldHandler;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [unit, setUnit] = useState(1);
+ const [error, setError] = useState<string>("");
+
+ const normal = normalize(handler.value, unit);
+ const previousValue = Amounts.stringifyValue(normal);
+
+ const [textValue, setTextValue] = useState<string>(previousValue);
+ useEffect(() => {
+ setTextValue(previousValue);
+ }, [previousValue]);
+
+ function updateUnit(newUnit: number) {
+ setUnit(newUnit);
+ const newNorm = normalize(handler.value, newUnit);
+ setTextValue(Amounts.stringifyValue(newNorm));
+ }
+
+ const currency = handler.value.currency;
+
+ const currencyLabels = buildLabelsForCurrency(
+ currency,
+ lowestDenom,
+ highestDenom,
+ );
+
+ function positiveAmount(value: string): string {
+ if (!value) {
+ if (handler.onInput) {
+ handler.onInput(Amounts.zeroOfCurrency(currency));
+ }
+ } else
+ try {
+ const parsed = Amounts.parseOrThrow(`${currency}:${value.trim()}`);
+
+ const realValue = denormalize(parsed, unit);
+
+ if (handler.onInput) {
+ handler.onInput(realValue);
+ }
+ setError("");
+ } catch (e) {
+ setError(i18n.str`Amount is not valid`);
+ }
+ setTextValue(value);
+ return value;
+ }
+
+ return (
+ <Fragment>
+ <TextField
+ label={label}
+ type="text"
+ min="0"
+ inputmode="decimal"
+ step="0.1"
+ variant="filled"
+ error={handler.error}
+ required={required}
+ startAdornment={
+ currencyLabels.length === 1 ? (
+ <div
+ style={{
+ marginTop: 20,
+ padding: "5px 12px 8px 12px",
+ }}
+ >
+ {currency}
+ </div>
+ ) : (
+ <select
+ disabled={!handler.onInput}
+ onChange={(e) => {
+ const unit = Number.parseFloat(e.currentTarget.value);
+ updateUnit(unit);
+ }}
+ value={String(unit)}
+ style={{
+ marginTop: 20,
+ padding: "5px 12px 8px 12px",
+ background: "transparent",
+ border: 0,
+ }}
+ >
+ {currencyLabels.map((c) => (
+ <option key={c} value={c.unit}>
+ <div>{c.name}</div>
+ </option>
+ ))}
+ </select>
+ )
+ }
+ value={textValue}
+ disabled={!handler.onInput}
+ onInput={positiveAmount}
+ />
+ {error && <div style={{ color: "red" }}>{error}</div>}
+ </Fragment>
+ );
+}
+
+/**
+ * Return the real value of a normalized unit
+ * If the value is 20 and the unit is kilo == 1000 the returned value will be amount * 1000
+ * @param amount
+ * @param unit
+ * @returns
+ */
+function denormalize(amount: AmountJson, unit: number): AmountJson {
+ if (unit === 1 || Amounts.isZero(amount)) return amount;
+ const result =
+ unit < 1
+ ? Amounts.divide(amount, 1 / unit)
+ : Amounts.mult(amount, unit).amount;
+ return result;
+}
+
+/**
+ * Return the amount in the current unit.
+ * If the value is 20000 and the unit is kilo == 1000 and the returned value will be amount / unit
+ *
+ * @param amount
+ * @param unit
+ * @returns
+ */
+function normalize(amount: AmountJson, unit: number): AmountJson {
+ if (unit === 1 || Amounts.isZero(amount)) return amount;
+ const result =
+ unit < 1
+ ? Amounts.mult(amount, 1 / unit).amount
+ : Amounts.divide(amount, unit);
+ return result;
+}
+
+/**
+ * Take every label in HIGH_DENOM_SYMBOL and LOW_DENOM_SYMBOL and create
+ * which create the corresponding unit multiplier
+ * @param currency
+ * @param lowestDenom
+ * @param highestDenom
+ * @returns
+ */
+function buildLabelsForCurrency(
+ currency: string,
+ lowestDenom: number,
+ highestDenom: number,
+): Array<{ name: string; unit: number }> {
+ let hd = Math.floor(Math.log10(highestDenom || 1) / 3);
+ let ld = Math.ceil((-1 * Math.log10(lowestDenom || 1)) / 3);
+
+ const result: Array<{ name: string; unit: number }> = [
+ {
+ name: currency,
+ unit: 1,
+ },
+ ];
+
+ while (hd > 0) {
+ result.push({
+ name: `${HIGH_DENOM_SYMBOL[hd]}${currency}`,
+ unit: Math.pow(10, hd * 3),
+ });
+ hd--;
+ }
+ while (ld > 0) {
+ result.push({
+ name: `${LOW_DENOM_SYMBOL[ld]}${currency}`,
+ unit: Math.pow(10, -1 * ld * 3),
+ });
+ ld--;
+ }
+ return result;
+}
diff --git a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
new file mode 100644
index 000000000..6dd577b88
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts, ScopeType, WalletBalance } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import {
+ TableWithRoundRows as TableWithRoundedRows
+} from "./styled/index.js";
+
+export function BalanceTable({
+ balances,
+ goToWalletHistory,
+}: {
+ balances: WalletBalance[];
+ goToWalletHistory: (currency: string) => void;
+}): VNode {
+ return (
+ <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%",
+ }}
+ >
+ {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
new file mode 100644
index 000000000..8b6377fc5
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
@@ -0,0 +1,360 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AmountJson,
+ Amounts,
+ parsePaytoUri,
+ segwitMinAmount,
+ stringifyPaytoUri,
+ TranslatedString,
+ WithdrawalExchangeAccountDetails,
+} from "@gnu-taler/taler-util";
+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";
+import { Amount } from "./Amount.js";
+import { ButtonBox, TooltipLeft, WarningBox } from "./styled/index.js";
+import { Button } from "../mui/Button.js";
+
+export interface BankDetailsProps {
+ subject: string;
+ amount: AmountJson;
+ accounts: WithdrawalExchangeAccountDetails[];
+}
+
+export function BankDetailsByPaytoType({
+ subject,
+ amount,
+ accounts: unsortedAccounts,
+}: BankDetailsProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [index, setIndex] = useState(0);
+
+ if (!unsortedAccounts.length) {
+ return <div>the exchange account list is empty</div>;
+ }
+
+ const accounts = unsortedAccounts.sort((a, b) => {
+ return (b.priority ?? 0) - (a.priority ?? 0);
+ });
+
+ const selectedAccount = accounts[index];
+ const altCurrency = selectedAccount.currencySpecification?.name;
+
+ const payto = parsePaytoUri(selectedAccount.paytoUri);
+
+ if (!payto) return <Fragment />;
+ payto.params["amount"] = altCurrency
+ ? selectedAccount.transferAmount!
+ : Amounts.stringify(amount);
+ payto.params["message"] = subject;
+
+ function Frame({
+ title,
+ children,
+ }: {
+ title: TranslatedString;
+ children: ComponentChildren;
+ }): VNode {
+ return (
+ <section
+ style={{
+ textAlign: "left",
+ border: "solid 1px black",
+ padding: 8,
+ borderRadius: 4,
+ }}
+ >
+ <div
+ style={{
+ display: "flex",
+ width: "100%",
+ justifyContent: "space-between",
+ }}
+ >
+ <p style={{ marginTop: 0 }}>{title}</p>
+ <div></div>
+ </div>
+
+ {children}
+
+ {accounts.length > 1 ? (
+ <Fragment>
+ {accounts.map((ac, acIdx) => {
+ const accountLabel = ac.bankLabel ?? `Account #${acIdx + 1}`;
+ return (
+ <Button
+ key={acIdx}
+ variant={acIdx === index ? "contained" : "outlined"}
+ onClick={async () => {
+ setIndex(acIdx);
+ }}
+ >
+ {accountLabel} (
+ {ac.currencySpecification?.name ?? amount.currency})
+ </Button>
+ );
+ })}
+
+ {/* <Button variant={currency === altCurrency ? "contained" : "outlined"}
+ onClick={async () => {
+ setCurrency(altCurrency)
+ }}
+ >
+ <i18n.Translate>{altCurrency}</i18n.Translate>
+ </Button> */}
+ </Fragment>
+ ) : undefined}
+ </section>
+ );
+ }
+
+ if (payto.isKnown && payto.targetType === "bitcoin") {
+ const min = segwitMinAmount(amount.currency);
+ const addrs = payto.segwitAddrs.map(
+ (a) => `${a} ${Amounts.stringifyValue(min)}`,
+ );
+ addrs.unshift(`${payto.targetPath} ${Amounts.stringifyValue(amount)}`);
+ const copyContent = addrs.join("\n");
+ return (
+ <Frame title={i18n.str`Bitcoin transfer details`}>
+ <p>
+ <i18n.Translate>
+ The exchange need a transaction with 3 output, one output is the
+ exchange account and the other two are segwit fake address for
+ metadata with an minimum amount.
+ </i18n.Translate>
+ </p>
+
+ <p>
+ <i18n.Translate>
+ In bitcoincore wallet use &apos;Add Recipient&apos; button to add
+ two additional recipient and copy addresses and amounts
+ </i18n.Translate>
+ </p>
+ <table>
+ <tr>
+ <td>
+ <div>
+ {payto.targetPath} <Amount value={amount} hideCurrency /> BTC
+ </div>
+ {payto.segwitAddrs.map((addr, i) => (
+ <div key={i}>
+ {addr} <Amount value={min} hideCurrency /> BTC
+ </div>
+ ))}
+ </td>
+ <td></td>
+ <td>
+ <CopyButton getContent={() => copyContent} />
+ </td>
+ </tr>
+ </table>
+ <p>
+ <i18n.Translate>
+ Make sure the amount show{" "}
+ {Amounts.stringifyValue(Amounts.sum([amount, min, min]).amount)}{" "}
+ BTC, else you have to change the base unit to BTC
+ </i18n.Translate>
+ </p>
+ </Frame>
+ );
+ }
+
+ const accountPart = !payto.isKnown ? (
+ <Fragment>
+ <Row name={i18n.str`Account`} value={payto.targetPath} />
+ </Fragment>
+ ) : payto.targetType === "x-taler-bank" ? (
+ <Fragment>
+ <Row name={i18n.str`Bank host`} value={payto.host} />
+ <Row name={i18n.str`Bank account`} value={payto.account} />
+ </Fragment>
+ ) : payto.targetType === "iban" ? (
+ <Fragment>
+ {payto.bic !== undefined ? (
+ <Row name={i18n.str`BIC`} value={payto.bic} />
+ ) : undefined}
+ <Row name={i18n.str`IBAN`} value={payto.iban} />
+ </Fragment>
+ ) : undefined;
+
+ const receiver =
+ payto.params["receiver-name"] || payto.params["receiver"] || undefined;
+ return (
+ <Frame title={i18n.str`Bank transfer details`}>
+ <table>
+ <tbody>
+ <tr>
+ <td colSpan={3}>
+ <i18n.Translate>Step 1:</i18n.Translate>
+ &nbsp;
+ <i18n.Translate>
+ Copy this code and paste it into the subject/purpose field in
+ your banking app or bank website
+ </i18n.Translate>
+ </td>
+ </tr>
+ <Row name={i18n.str`Subject`} value={subject} literal />
+
+ <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
+ or the "payto://" URI below to copy just one value.
+ </i18n.Translate>
+ </span>
+ </WarningBox>
+ </td>
+ </tr>
+
+ <tr>
+ <td colSpan={2} width="100%" style={{ wordBreak: "break-all" }}>
+ <i18n.Translate>
+ Alternative if your bank already supports PayTo URI, you can use
+ this{" "}
+ <a
+ target="_bank"
+ rel="noreferrer"
+ title="RFC 8905 for designating targets for payments"
+ href="https://tools.ietf.org/html/rfc8905"
+ >
+ PayTo URI
+ </a>{" "}
+ link instead
+ </i18n.Translate>
+ </td>
+ <td>
+ <CopyButton getContent={() => stringifyPaytoUri(payto)} />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </Frame>
+ );
+}
+
+function CopyButton({ getContent }: { getContent: () => string }): VNode {
+ const [copied, setCopied] = useState(false);
+ function copyText(): void {
+ navigator.clipboard.writeText(getContent() || "");
+ setCopied(true);
+ }
+ useEffect(() => {
+ if (copied) {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }
+ }, [copied]);
+
+ if (!copied) {
+ return (
+ <ButtonBox onClick={copyText}>
+ <CopyIcon />
+ </ButtonBox>
+ );
+ }
+ return (
+ <TooltipLeft content="Copied">
+ <ButtonBox disabled>
+ <CopiedIcon />
+ </ButtonBox>
+ </TooltipLeft>
+ );
+}
+
+function Row({
+ name,
+ value,
+ literal,
+}: {
+ name: TranslatedString;
+ value: string | VNode;
+ literal?: boolean;
+}): VNode {
+ const preRef = useRef<HTMLPreElement>(null);
+ const tdRef = useRef<HTMLTableCellElement>(null);
+
+ function getContent(): string {
+ return preRef.current?.textContent || tdRef.current?.textContent || "";
+ }
+
+ return (
+ <tr>
+ <td style={{ paddingRight: 8 }}>
+ <b>{name}</b>
+ </td>
+ {literal ? (
+ <td>
+ <pre
+ ref={preRef}
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {value}
+ </pre>
+ </td>
+ ) : (
+ <td ref={tdRef}>{value}</td>
+ )}
+ <td>
+ <CopyButton getContent={getContent} />
+ </td>
+ </tr>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/Banner.stories.tsx b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
new file mode 100644
index 000000000..ee2dbfc69
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
@@ -0,0 +1,126 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Fragment, h, VNode } from "preact";
+import { Avatar } from "../mui/Avatar.js";
+import { Typography } from "../mui/Typography.js";
+import { Banner } from "./Banner.js";
+import { SvgIcon } from "./styled/index.js";
+import wifiIcon from "../svg/wifi.inline.svg";
+export default {
+ title: "banner",
+ component: Banner,
+};
+
+function Wrapper({ children }: any): VNode {
+ return (
+ <div
+ style={{
+ display: "flex",
+ backgroundColor: "lightgray",
+ padding: 10,
+ width: "100%",
+ // width: 400,
+ // height: 400,
+ justifyContent: "center",
+ }}
+ >
+ <div style={{ flexGrow: 1 }}>{children}</div>
+ </div>
+ );
+}
+function SignalWifiOffIcon({ ...rest }: any): VNode {
+ return <SvgIcon {...rest} dangerouslySetInnerHTML={{ __html: wifiIcon }} />;
+}
+
+export const BasicExample = (): VNode => (
+ <Fragment>
+ <Wrapper>
+ <p>
+ Example taken from:
+ <a
+ target="_blank"
+ rel="noreferrer"
+ href="https://medium.com/material-ui/introducing-material-ui-design-system-93e921beb8df"
+ >
+ https://medium.com/material-ui/introducing-material-ui-design-system-93e921beb8df
+ </a>
+ </p>
+ <Banner
+ // elements={[
+ // {
+ // icon: <SignalWifiOffIcon color="gray" />,
+ // description: (
+ // <Typography>
+ // You have lost connection to the internet. This app is offline.
+ // </Typography>
+ // ),
+ // },
+ // ]}
+ confirm={{
+ label: "turn on wifi",
+ action: async () => {
+ return;
+ },
+ }}
+ >
+ <div />
+ </Banner>
+ </Wrapper>
+ </Fragment>
+);
+
+export const PendingOperation = (): VNode => (
+ <Fragment>
+ <Wrapper>
+ <Banner
+ title="PENDING TRANSACTIONS"
+ style={{ backgroundColor: "lightcyan", padding: 8 }}
+ // elements={[
+ // {
+ // icon: (
+ // <Avatar
+ // style={{
+ // border: "solid blue 1px",
+ // color: "blue",
+ // boxSizing: "border-box",
+ // }}
+ // >
+ // P
+ // </Avatar>
+ // ),
+ // description: (
+ // <Fragment>
+ // <Typography inline bold>
+ // EUR 37.95
+ // </Typography>
+ // &nbsp;
+ // <Typography inline>- 5 feb 2022</Typography>
+ // </Fragment>
+ // ),
+ // },
+ // ]}
+ >
+ asd
+ </Banner>
+ </Wrapper>
+ </Fragment>
+);
diff --git a/packages/taler-wallet-webextension/src/components/Banner.tsx b/packages/taler-wallet-webextension/src/components/Banner.tsx
new file mode 100644
index 000000000..40a4847b8
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Banner.tsx
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { ComponentChildren, Fragment, h, JSX, VNode } from "preact";
+import { Button } from "../mui/Button.js";
+import { Divider } from "../mui/Divider.js";
+import { Grid } from "../mui/Grid.js";
+import { Paper } from "../mui/Paper.js";
+
+interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
+ titleHead?: VNode | TranslatedString;
+ children: ComponentChildren;
+ // elements: {
+ // icon?: VNode;
+ // description: VNode;
+ // action?: () => void;
+ // }[];
+ confirm?: {
+ label: string;
+ action: () => Promise<void>;
+ };
+}
+
+export function Banner({
+ titleHead,
+ children,
+ confirm,
+ href,
+ ...rest
+}: Props): VNode {
+ return (
+ <Fragment>
+ <Paper elevation={0} {...rest}>
+ {titleHead && (
+ <Grid container>
+ <Grid item>{titleHead}</Grid>
+ </Grid>
+ )}
+ <Grid container columns={1}>
+ {children}
+ </Grid>
+ {confirm && (
+ <Grid container justifyContent="flex-end" spacing={8}>
+ <Grid item>
+ <Button color="primary" onClick={confirm.action}>
+ {confirm.label}
+ </Button>
+ </Grid>
+ </Grid>
+ )}
+ </Paper>
+ <Divider />
+ </Fragment>
+ );
+}
+
+export default Banner;
diff --git a/packages/taler-wallet-webextension/src/components/Checkbox.tsx b/packages/taler-wallet-webextension/src/components/Checkbox.tsx
index 2d7b98087..ec1b93a01 100644
--- a/packages/taler-wallet-webextension/src/components/Checkbox.tsx
+++ b/packages/taler-wallet-webextension/src/components/Checkbox.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 Taler Systems SA
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,17 +14,24 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { JSX } from "preact/jsx-runtime";
-import { h } from "preact";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
interface Props {
- enabled: boolean;
- onToggle: () => void;
- label: string;
+ enabled?: boolean;
+ onToggle?: () => Promise<void>;
+ label: TranslatedString;
name: string;
- description?: string;
+ description?: VNode | TranslatedString;
}
-export function Checkbox({ name, enabled, onToggle, label, description }: Props): JSX.Element {
+export function Checkbox({
+ name,
+ enabled,
+ onToggle,
+ label,
+ description,
+}: Props): VNode {
+
return (
<div>
<input
@@ -32,23 +39,26 @@ export function Checkbox({ name, enabled, onToggle, label, description }: Props)
onClick={onToggle}
type="checkbox"
id={`checkbox-${name}`}
- style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }} />
+ style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }}
+ />
<label
htmlFor={`checkbox-${name}`}
style={{ marginLeft: "0.5em", fontWeight: "bold" }}
>
{label}
</label>
- {description && <span
- style={{
- color: "#383838",
- fontSize: "smaller",
- display: "block",
- marginLeft: "2em",
- }}
- >
- {description}
- </span>}
+ {description && (
+ <span
+ style={{
+ color: "#383838",
+ fontSize: "smaller",
+ display: "block",
+ marginLeft: "2em",
+ }}
+ >
+ {description}
+ </span>
+ )}
</div>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx b/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx
index 5e30ee3d1..79712c2f4 100644
--- a/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx
+++ b/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 Taler Systems SA
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,45 +14,55 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { JSX } from "preact/jsx-runtime";
-import { Outlined, StyledCheckboxLabel } from "./styled/index";
-import { h } from 'preact';
+import { Outlined, StyledCheckboxLabel } from "./styled/index.js";
+import { h, VNode } from "preact";
interface Props {
- enabled: boolean;
- onToggle: () => void;
- label: string;
+ enabled?: boolean;
+ onToggle?: () => Promise<void>;
+ label: VNode;
name: string;
}
+const Tick = (): VNode => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ focusable="false"
+ style={{ backgroundColor: "green" }}
+ >
+ <path
+ fill="none"
+ stroke="white"
+ stroke-width="3"
+ d="M1.73 12.91l6.37 6.37L22.79 4.59"
+ />
+ </svg>
+);
-const Tick = () => <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 24 24"
- aria-hidden="true"
- focusable="false"
- style={{ backgroundColor: 'green' }}
->
- <path
- fill="none"
- stroke="white"
- stroke-width="3"
- d="M1.73 12.91l6.37 6.37L22.79 4.59"
- />
-</svg>
-
-export function CheckboxOutlined({ name, enabled, onToggle, label }: Props): JSX.Element {
+export function CheckboxOutlined({
+ name,
+ enabled,
+ onToggle,
+ label,
+}: Props): VNode {
return (
- <Outlined>
- <StyledCheckboxLabel onClick={onToggle}>
+ <StyledCheckboxLabel onClick={onToggle}>
+ <Outlined>
<span>
- <input type="checkbox" name={name} checked={enabled} disabled={false} />
+ <input
+ type="checkbox"
+ name={name}
+ checked={enabled}
+ disabled={false}
+ />
<div>
<Tick />
</div>
<label for={name}>{label}</label>
</span>
- </StyledCheckboxLabel>
- </Outlined>
+ </Outlined>
+ </StyledCheckboxLabel>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/CopyButton.tsx b/packages/taler-wallet-webextension/src/components/CopyButton.tsx
new file mode 100644
index 000000000..2024e2423
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/CopyButton.tsx
@@ -0,0 +1,54 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { CopiedIcon, CopyIcon } from "../svg/index.js";
+import { ButtonBox, TooltipLeft } from "./styled/index.js";
+
+export function CopyButton({
+ getContent,
+}: {
+ getContent: () => string;
+}): VNode {
+ const [copied, setCopied] = useState(false);
+ function copyText(): void {
+ navigator.clipboard.writeText(getContent() || "");
+ setCopied(true);
+ }
+ useEffect(() => {
+ if (copied) {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }
+ }, [copied]);
+
+ if (!copied) {
+ return (
+ <ButtonBox onClick={copyText}>
+ <CopyIcon />
+ </ButtonBox>
+ );
+ }
+ return (
+ <TooltipLeft content="Copied">
+ <ButtonBox disabled>
+ <CopiedIcon />
+ </ButtonBox>
+ </TooltipLeft>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx b/packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx
new file mode 100644
index 000000000..b1ed3b02c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/CurrentAlerts.tsx
@@ -0,0 +1,147 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import {
+ Alert as AlertNotification,
+ useAlertContext,
+} from "../context/alert.js";
+import { Alert } from "../mui/Alert.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+
+/**
+ *
+ * @author sebasjm
+ */
+
+function AlertContext({
+ context,
+ cause,
+}: {
+ cause: unknown;
+ context: undefined | object;
+}): VNode {
+ const [more, setMore] = useState(false);
+ const [wrap, setWrap] = useState(false);
+ const { i18n } = useTranslationContext();
+ if (!more) {
+ return (
+ <div style={{ display: "flex", justifyContent: "right" }}>
+ <a
+ onClick={() => setMore(true)}
+ style={{ cursor: "pointer", textDecoration: "underline" }}
+ >
+ <i18n.Translate>more info</i18n.Translate>
+ </a>
+ </div>
+ );
+ }
+ const errorInfo = JSON.stringify(
+ context === undefined ? { cause } : { context, cause },
+ undefined,
+ 2,
+ );
+ return (
+ <Fragment>
+ <div style={{ display: "flex", justifyContent: "right" }}>
+ <a
+ onClick={() => setWrap(!wrap)}
+ style={{ cursor: "pointer", textDecoration: "underline" }}
+ >
+ <i18n.Translate>wrap text</i18n.Translate>
+ </a>
+ &nbsp;&nbsp;
+ <a
+ onClick={() => navigator.clipboard.writeText(errorInfo)}
+ style={{ cursor: "pointer", textDecoration: "underline" }}
+ >
+ <i18n.Translate>copy content</i18n.Translate>
+ </a>
+ &nbsp;&nbsp;
+ <a
+ onClick={() => setMore(false)}
+ style={{ cursor: "pointer", textDecoration: "underline" }}
+ >
+ <i18n.Translate>less info</i18n.Translate>
+ </a>
+ </div>
+ <pre
+ style={
+ wrap
+ ? {
+ whiteSpace: "pre-wrap",
+ overflowWrap: "anywhere",
+ }
+ : {
+ overflow: "overlay",
+ }
+ }
+ >
+ {errorInfo}
+ </pre>
+ </Fragment>
+ );
+}
+
+export function ErrorAlertView({
+ error,
+ onClose,
+}: {
+ error: AlertNotification;
+ onClose?: () => Promise<void>;
+}): VNode {
+ return (
+ <Wrapper>
+ <AlertView alert={error} onClose={onClose} />
+ </Wrapper>
+ );
+}
+
+export function AlertView({
+ alert,
+ onClose,
+}: {
+ alert: AlertNotification;
+ onClose?: () => Promise<void>;
+}): VNode {
+ return (
+ <Alert title={alert.message} severity={alert.type} onClose={onClose}>
+ <div style={{ display: "flex", flexDirection: "column" }}>
+ <div>{alert.description}</div>
+ {alert.type === "error" && alert.cause !== undefined ? (
+ <AlertContext context={alert.context} cause={alert.cause} />
+ ) : undefined}
+ </div>
+ </Alert>
+ );
+}
+
+export function CurrentAlerts(): VNode {
+ const { alerts, removeAlert } = useAlertContext();
+ if (alerts.length === 0) return <Fragment />;
+ return (
+ <Wrapper>
+ {alerts.map((n, i) => (
+ <AlertView key={i} alert={n} onClose={async () => removeAlert(n)} />
+ ))}
+ </Wrapper>
+ );
+}
+
+function Wrapper({ children }: { children: ComponentChildren }): VNode {
+ return <div style={{ margin: "1em" }}>{children}</div>;
+}
diff --git a/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx b/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx
deleted file mode 100644
index f0c682ccb..000000000
--- a/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
- import { JSX, h } from "preact";
-
-export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean; onToggle: () => void; }): JSX.Element {
- return (
- <div>
- <input
- checked={enabled}
- onClick={onToggle}
- type="checkbox"
- id="checkbox-perm"
- style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }} />
- <label
- htmlFor="checkbox-perm"
- style={{ marginLeft: "0.5em", fontWeight: "bold" }}
- >
- Automatically open wallet based on page content
- </label>
- <span
- style={{
- color: "#383838",
- fontSize: "smaller",
- display: "block",
- marginLeft: "2em",
- }}
- >
- (Enabling this option below will make using the wallet faster, but
- requires more permissions from your browser.)
- </span>
- </div>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx
index b48deb847..8bd0abcaf 100644
--- a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx
+++ b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,59 +15,72 @@
*/
import { WalletDiagnostics } from "@gnu-taler/taler-util";
-import { h } from "preact";
-import { JSX } from "preact/jsx-runtime";
-import { PageLink } from "../renderHtml";
+import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
interface Props {
timedOut: boolean;
- diagnostics: WalletDiagnostics | undefined
+ diagnostics: WalletDiagnostics | undefined;
}
-export function Diagnostics({timedOut, diagnostics}: Props): JSX.Element | null {
-
+export function Diagnostics({ timedOut, diagnostics }: Props): VNode {
+ const { i18n } = useTranslationContext();
if (timedOut) {
- return <p>Diagnostics timed out. Could not talk to the wallet backend.</p>;
+ return (
+ <p>
+ <i18n.Translate>
+ Diagnostics timed out. Could not talk to the wallet backend.
+ </i18n.Translate>
+ </p>
+ );
}
if (diagnostics) {
if (diagnostics.errors.length === 0) {
- return null;
- } else {
- return (
- <div
- style={{
- borderLeft: "0.5em solid red",
- paddingLeft: "1em",
- paddingTop: "0.2em",
- paddingBottom: "0.2em",
- }}
- >
- <p>Problems detected:</p>
- <ol>
- {diagnostics.errors.map((errMsg) => (
- <li key={errMsg}>{errMsg}</li>
- ))}
- </ol>
- {diagnostics.firefoxIdbProblem ? (
- <p>
+ return <Fragment />;
+ }
+ return (
+ <div
+ style={{
+ borderLeft: "0.5em solid red",
+ paddingLeft: "1em",
+ paddingTop: "0.2em",
+ paddingBottom: "0.2em",
+ }}
+ >
+ <p>
+ <i18n.Translate>Problems detected:</i18n.Translate>
+ </p>
+ <ol>
+ {diagnostics.errors.map((errMsg) => (
+ <li key={errMsg}>{errMsg}</li>
+ ))}
+ </ol>
+ {diagnostics.firefoxIdbProblem ? (
+ <p>
+ <i18n.Translate>
Please check in your <code>about:config</code> settings that you
have IndexedDB enabled (check the preference name{" "}
<code>dom.indexedDB.enabled</code>).
- </p>
- ) : null}
- {diagnostics.dbOutdated ? (
- <p>
+ </i18n.Translate>
+ </p>
+ ) : null}
+ {diagnostics.dbOutdated ? (
+ <p>
+ <i18n.Translate>
Your wallet database is outdated. Currently automatic migration is
- not supported. Please go{" "}
- <PageLink pageName="/reset-required">here</PageLink> to reset
- the wallet database.
- </p>
- ) : null}
- </div>
- );
- }
+ not supported. Please go <i18n.Translate>here</i18n.Translate>
+ to reset the wallet database.
+ </i18n.Translate>
+ </p>
+ ) : null}
+ </div>
+ );
}
- return <p>Running diagnostics ...</p>;
+ return (
+ <p>
+ <i18n.Translate>Running diagnostics</i18n.Translate> ...
+ </p>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/components/EditableText.tsx b/packages/taler-wallet-webextension/src/components/EditableText.tsx
index 6f3388bf9..1da090492 100644
--- a/packages/taler-wallet-webextension/src/components/EditableText.tsx
+++ b/packages/taler-wallet-webextension/src/components/EditableText.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 Taler Systems SA
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,9 +14,9 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { h } from "preact";
+import { h, VNode } from "preact";
import { useRef, useState } from "preact/hooks";
-import { JSX } from "preact/jsx-runtime";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
interface Props {
value: string;
@@ -25,25 +25,44 @@ interface Props {
name: string;
description?: string;
}
-export function EditableText({ name, value, onChange, label, description }: Props): JSX.Element {
- const [editing, setEditing] = useState(false)
- const ref = useRef<HTMLInputElement>(null)
+export function EditableText({
+ name,
+ value,
+ onChange,
+ label,
+ description,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [editing, setEditing] = useState(false);
+ const ref = useRef<HTMLInputElement>(null);
let InputText;
if (!editing) {
- InputText = () => <div style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p>{value}</p>
- <button onClick={() => setEditing(true)}>edit</button>
- </div>
+ InputText = function InputToEdit(): VNode {
+ return (
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <p>{value}</p>
+ <button onClick={() => setEditing(true)}>
+ <i18n.Translate>Edit</i18n.Translate>
+ </button>
+ </div>
+ );
+ };
} else {
- InputText = () => <div style={{ display: 'flex', justifyContent: 'space-between' }}>
- <input
- value={value}
- ref={ref}
- type="text"
- id={`text-${name}`}
- />
- <button onClick={() => { if (ref.current) onChange(ref.current.value).then(r => setEditing(false)) }}>confirm</button>
- </div>
+ InputText = function InputEditing(): VNode {
+ return (
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <input value={value} ref={ref} type="text" id={`text-${name}`} />
+ <button
+ onClick={() => {
+ if (ref.current)
+ onChange(ref.current.value).then(() => setEditing(false));
+ }}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </button>
+ </div>
+ );
+ };
}
return (
<div>
@@ -54,16 +73,18 @@ export function EditableText({ name, value, onChange, label, description }: Prop
{label}
</label>
<InputText />
- {description && <span
- style={{
- color: "#383838",
- fontSize: "smaller",
- display: "block",
- marginLeft: "2em",
- }}
- >
- {description}
- </span>}
+ {description && (
+ <span
+ style={{
+ color: "#383838",
+ fontSize: "smaller",
+ display: "block",
+ marginLeft: "2em",
+ }}
+ >
+ {description}
+ </span>
+ )}
</div>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/EnabledBySettings.tsx b/packages/taler-wallet-webextension/src/components/EnabledBySettings.tsx
new file mode 100644
index 000000000..6f666d301
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/EnabledBySettings.tsx
@@ -0,0 +1,38 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { useSettings } from "../hooks/useSettings.js";
+import { Settings } from "../platform/api.js";
+
+export function EnabledBySettings<K extends keyof Settings>({
+ children,
+ value,
+ name,
+}: {
+ name: K;
+ value?: Settings[K];
+ children: ComponentChildren;
+}): VNode {
+ const [settings] = useSettings();
+ if (value === undefined) {
+ if (!settings[name]) return <Fragment />;
+ return <Fragment>{children}</Fragment>;
+ }
+ if (settings[name] !== value) {
+ return <Fragment />;
+ }
+ return <Fragment>{children}</Fragment>;
+}
diff --git a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
index cfcef16d5..06c8a81ef 100644
--- a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
+++ b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 Taler Systems SA
+ (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
@@ -13,22 +13,48 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- import { VNode, h } from "preact";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import arrowDown from '../../static/img/chevron-down.svg';
-import { ErrorBox } from "./styled";
+import arrowDown from "../svg/chevron-down.inline.svg";
+import { ErrorBox } from "./styled/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
-export function ErrorMessage({ title, description }: { title?: string|VNode; description?: string; }) {
+export function ErrorMessage({
+ title,
+ description,
+}: {
+ title: TranslatedString;
+ description?: string | VNode | Error;
+}): VNode | null {
const [showErrorDetail, setShowErrorDetail] = useState(false);
- if (!title)
- return null;
- return <ErrorBox style={{paddingTop: 0, paddingBottom: 0}}>
- <div>
- <p>{title}</p>
- { description && <button onClick={() => { setShowErrorDetail(v => !v); }}>
- <img style={{ height: '1.5em' }} src={arrowDown} />
- </button> }
- </div>
- {showErrorDetail && <p>{description}</p>}
- </ErrorBox>;
+ const [showMore, setShowMore] = useState(false);
+ const { i18n } = useTranslationContext();
+ return (
+ <ErrorBox style={{ paddingTop: 0, paddingBottom: 0 }}>
+ <div>
+ <p>{title}</p>
+ {description && (
+ <button
+ onClick={() => {
+ setShowErrorDetail((v) => !v);
+ }}
+ >
+ <div
+ style={{ height: "1.5em" }}
+ dangerouslySetInnerHTML={{ __html: arrowDown }}
+ />
+ </button>
+ )}
+ </div>
+ {showErrorDetail && description && <p>
+ {description instanceof Error && !showMore ? description.message : description.toString()}
+ {description instanceof Error && <div>
+ <a href="#" onClick={(e) => {
+ setShowMore(!showMore)
+ e.preventDefault()
+ }}>{showMore ? i18n.str`show less` : i18n.str`show more`} </a> </div>}
+ </p>}
+ </ErrorBox>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx b/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx
new file mode 100644
index 000000000..3298840e2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx
@@ -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 { TalerErrorDetail, TranslatedString } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import arrowDown from "../svg/chevron-down.inline.svg";
+import { ErrorBox } from "./styled/index.js";
+import { EnabledBySettings } from "./EnabledBySettings.js";
+
+export function ErrorTalerOperation({
+ title,
+ error,
+}: {
+ title?: TranslatedString;
+ error?: TalerErrorDetail;
+}): VNode | null {
+ const [showErrorDetail, setShowErrorDetail] = useState(false);
+
+ if (!title || !error) return null;
+ // const errorCode: number | undefined = (error.details as any)?.errorResponse?.code
+ const errorHint: string | undefined = (error.details as any)?.errorResponse
+ ?.hint;
+
+ return (
+ <ErrorBox style={{ paddingTop: 0, paddingBottom: 0 }}>
+ <div>
+ <p>{title}</p>
+ {error && (
+ <button
+ onClick={() => {
+ setShowErrorDetail((v) => !v);
+ }}
+ >
+ <div
+ style={{
+ transform: !showErrorDetail ? undefined : "scaleY(-1)",
+ height: 24,
+ }}
+ dangerouslySetInnerHTML={{ __html: arrowDown }}
+ />
+ </button>
+ )}
+ </div>
+ {showErrorDetail && (
+ <Fragment>
+ <div style={{ padding: 5, textAlign: "left" }}>
+ <div>
+ <b>{error.hint}</b> {!errorHint ? "" : `: ${errorHint}`}{" "}
+ </div>
+ </div>
+ <EnabledBySettings name="showJsonOnError">
+ <div style={{ textAlign: "left", overflowX: "auto" }}>
+ <pre>{JSON.stringify(error, undefined, 2)}</pre>
+ </div>
+ </EnabledBySettings>
+ </Fragment>
+ )}
+ </ErrorBox>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx b/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx
index cfa20280f..e7247ba33 100644
--- a/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx
+++ b/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 Taler Systems SA
+ (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
@@ -13,66 +13,82 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- import { Fragment, VNode } from "preact"
-import { useState } from "preact/hooks"
-import { JSXInternal } from "preact/src/jsx"
-import { h } from 'preact';
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
-export function ExchangeXmlTos({ doc }: { doc: Document }) {
- const termsNode = doc.querySelector('[ids=terms-of-service]')
+export function ExchangeXmlTos({ doc }: { doc: Document }): VNode {
+ if (typeof window === "undefined") {
+ // in nodejs env we don't have xml api
+ return <div />;
+ }
+ const termsNode = doc.querySelector("[ids=terms-of-service]");
if (!termsNode) {
- return <div>
- <p>The exchange send us an xml but there is no node with 'ids=terms-of-service'. This is the content:</p>
- <pre>{new XMLSerializer().serializeToString(doc)}</pre>
- </div>
+ return (
+ <div>
+ <p>
+ The exchange send us an xml but there is no node with
+ &apos;ids=terms-of-service&apos;. This is the content:
+ </p>
+ <pre>{new XMLSerializer().serializeToString(doc)}</pre>
+ </div>
+ );
}
- return <Fragment>
- {Array.from(termsNode.children).map(renderChild)}
- </Fragment>
+ return <Fragment>{Array.from(termsNode.children).map(renderChild)}</Fragment>;
}
/**
* Map XML elements into HTML
- * @param child
- * @returns
+ * @param child
+ * @returns
*/
function renderChild(child: Element): VNode {
- const children = Array.from(child.children)
+ const children = Array.from(child.children);
switch (child.nodeName) {
- case 'title': return <header>{child.textContent}</header>
- case '#text': return <Fragment />
- case 'paragraph': return <p>{child.textContent}</p>
- case 'section': {
- return <AnchorWithOpenState href={`#terms-${child.getAttribute('ids')}`}>
- {children.map(renderChild)}
- </AnchorWithOpenState>
+ case "title":
+ return <header>{child.textContent}</header>;
+ case "#text":
+ return <Fragment />;
+ case "paragraph":
+ return <p>{child.textContent}</p>;
+ case "section": {
+ return (
+ <AnchorWithOpenState href={`#terms-${child.getAttribute("ids")}`}>
+ {children.map(renderChild)}
+ </AnchorWithOpenState>
+ );
}
- case 'bullet_list': {
- return <ul>{children.map(renderChild)}</ul>
+ case "bullet_list": {
+ return <ul>{children.map(renderChild)}</ul>;
}
- case 'enumerated_list': {
- return <ol>{children.map(renderChild)}</ol>
+ case "enumerated_list": {
+ return <ol>{children.map(renderChild)}</ol>;
}
- case 'list_item': {
- return <li>{children.map(renderChild)}</li>
+ case "list_item": {
+ return <li>{children.map(renderChild)}</li>;
}
- case 'block_quote': {
- return <div>{children.map(renderChild)}</div>
+ case "block_quote": {
+ return <div>{children.map(renderChild)}</div>;
}
- default: return <div style={{ color: 'red', display: 'hidden' }}>unknown tag {child.nodeName} <a></a></div>
+ default:
+ return (
+ <div style={{ color: "red", display: "hidden" }}>
+ unknown tag {child.nodeName}
+ </div>
+ );
}
}
/**
* Simple anchor with a state persisted into 'data-open' prop
- * @returns
+ * @returns
*/
-function AnchorWithOpenState(props: JSXInternal.HTMLAttributes<HTMLAnchorElement>) {
- const [open, setOpen] = useState<boolean>(false)
- function doClick(e: JSXInternal.TargetedMouseEvent<HTMLAnchorElement>) {
+function AnchorWithOpenState(
+ props: h.JSX.HTMLAttributes<HTMLAnchorElement>,
+): VNode {
+ const [open, setOpen] = useState<boolean>(false);
+ function doClick(e: h.JSX.TargetedMouseEvent<HTMLAnchorElement>): void {
setOpen(!open);
e.preventDefault();
}
- return <a data-open={open ? 'true' : 'false'} onClick={doClick} {...props} />
+ return <a data-open={open ? "true" : "false"} onClick={doClick} {...props} />;
}
-
diff --git a/packages/taler-wallet-webextension/src/components/HistoryItem.tsx b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
new file mode 100644
index 000000000..9be9326b2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/HistoryItem.tsx
@@ -0,0 +1,432 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AmountJson,
+ Amounts,
+ AmountString,
+ AbsoluteTime,
+ Transaction,
+ TransactionType,
+ WithdrawalType,
+ TransactionMajorState,
+ DenomLossEventType,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Avatar } from "../mui/Avatar.js";
+import { Pages } from "../NavigationBar.js";
+import { assertUnreachable } from "../utils/index.js";
+import {
+ Column,
+ ExtraLargeText,
+ HistoryRow,
+ LargeText,
+ LightText,
+ SmallLightText,
+} from "./styled/index.js";
+import { Time } from "./Time.js";
+
+export function HistoryItem(props: { tx: Transaction }): VNode {
+ const tx = props.tx;
+ const { i18n } = useTranslationContext();
+ /**
+ *
+ */
+ switch (tx.type) {
+ case TransactionType.Withdrawal:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ title={new URL(tx.exchangeBaseUrl).hostname}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"W"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? tx.withdrawalDetails.type ===
+ WithdrawalType.TalerBankIntegrationApi
+ ? !tx.withdrawalDetails.confirmed
+ ? i18n.str`Need approval in the Bank`
+ : i18n.str`Waiting for wire transfer to complete`
+ : tx.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ ? i18n.str`Waiting for wire transfer to complete`
+ : "" //pending but no message
+ : undefined
+ }
+ />
+ );
+ case TransactionType.InternalWithdrawal:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ title={new URL(tx.exchangeBaseUrl).hostname}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"I"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? tx.withdrawalDetails.type ===
+ WithdrawalType.TalerBankIntegrationApi
+ ? !tx.withdrawalDetails.confirmed
+ ? i18n.str`Need approval in the Bank`
+ : i18n.str`Exchange is waiting the wire transfer`
+ : tx.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ ? i18n.str`Exchange is waiting the wire transfer`
+ : "" //pending but no message
+ : undefined
+ }
+ />
+ );
+ case TransactionType.Payment:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={tx.info.merchant.name}
+ subtitle={tx.info.summary}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"P"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Payment in progress`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.Refund:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ subtitle={tx.paymentInfo ? tx.paymentInfo.summary : undefined} //FIXME: DD37 wallet-core is not returning this value
+ title={
+ tx.paymentInfo
+ ? tx.paymentInfo.merchant.name
+ : "--unknown merchant--"
+ } //FIXME: DD37 wallet-core is not returning this value
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"R"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Executing refund...`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.Refresh:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ title={"Refresh"}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"R"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Refreshing coins...`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.Deposit:{
+ const payto = parsePaytoUri(tx.targetPaytoUri);
+ const title = payto === undefined || !payto.isKnown ? tx.targetPaytoUri :
+ payto.params["receiver-name"] ;
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={title}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"D"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Deposit in progress`
+ : undefined
+ }
+ />
+ );
+ }
+ case TransactionType.PeerPullCredit:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ title={tx.info.summary || "Invoice"}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"I"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Waiting to be paid`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.PeerPullDebit:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={tx.info.summary || "Invoice"}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"I"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Payment in progress`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.PeerPushCredit:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"credit"}
+ title={tx.info.summary || "Transfer"}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"T"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Receiving the transfer`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.PeerPushDebit:
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={tx.info.summary || "Transfer"}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"T"}
+ currentState={tx.txState.major}
+ description={
+ tx.txState.major === TransactionMajorState.Pending
+ ? i18n.str`Waiting to be received`
+ : undefined
+ }
+ />
+ );
+ case TransactionType.DenomLoss: {
+ switch (tx.lossEventType) {
+ case DenomLossEventType.DenomExpired: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination expired`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ case DenomLossEventType.DenomVanished: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination vanished`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ case DenomLossEventType.DenomUnoffered: {
+ return (
+ <Layout
+ id={tx.transactionId}
+ amount={tx.amountEffective}
+ debitCreditIndicator={"debit"}
+ title={i18n.str`Denomination unoffered`}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(tx.timestamp)}
+ iconPath={"L"}
+ currentState={tx.txState.major}
+ description={undefined}
+ />
+ );
+ }
+ default: {
+ assertUnreachable(tx.lossEventType);
+ }
+ }
+ break;
+ }
+ case TransactionType.Recoup:
+ throw Error("recoup transaction not implemented");
+ default: {
+ assertUnreachable(tx);
+ }
+ }
+}
+
+function Layout(props: LayoutProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <HistoryRow
+ href={Pages.balanceTransaction({ tid: props.id })}
+ style={{
+ backgroundColor:
+ props.currentState === TransactionMajorState.Pending ||
+ props.currentState === TransactionMajorState.Dialog
+ ? "lightcyan"
+ : props.currentState === TransactionMajorState.Failed
+ ? "#ff000040"
+ : props.currentState === TransactionMajorState.Aborted ||
+ props.currentState === TransactionMajorState.Aborting
+ ? "#00000010"
+ : "inherit",
+ alignItems: "center",
+ }}
+ >
+ <Avatar
+ style={{
+ border: "solid gray 1px",
+ color: "gray",
+ boxSizing: "border-box",
+ }}
+ >
+ {props.iconPath}
+ </Avatar>
+ <Column>
+ <LargeText>
+ <div>{props.title}</div>
+ {props.subtitle && (
+ <div style={{ color: "gray", fontSize: "medium", marginTop: 5 }}>
+ {props.subtitle}
+ </div>
+ )}
+ </LargeText>
+ {props.description && (
+ <LightText style={{ marginTop: 5, marginBottom: 5 }}>
+ <i18n.Translate>{props.description}</i18n.Translate>
+ </LightText>
+ )}
+ <SmallLightText style={{ marginTop: 5 }}>
+ <Time timestamp={props.timestamp} format="HH:mm" />
+ </SmallLightText>
+ </Column>
+ <TransactionAmount
+ currentState={props.currentState}
+ amount={Amounts.parseOrThrow(props.amount)}
+ debitCreditIndicator={props.debitCreditIndicator}
+ />
+ </HistoryRow>
+ );
+}
+
+interface LayoutProps {
+ debitCreditIndicator: "debit" | "credit" | "unknown";
+ amount: AmountString | "unknown";
+ timestamp: AbsoluteTime;
+ title: string;
+ subtitle?: string;
+ id: string;
+ iconPath: string;
+ currentState: TransactionMajorState;
+ description?: string;
+}
+
+interface TransactionAmountProps {
+ debitCreditIndicator: "debit" | "credit" | "unknown";
+ amount: AmountJson;
+ currentState: TransactionMajorState;
+}
+
+function TransactionAmount(props: TransactionAmountProps): VNode {
+ const { i18n } = useTranslationContext();
+ let sign: string;
+ switch (props.debitCreditIndicator) {
+ case "credit":
+ sign = "+";
+ break;
+ case "debit":
+ sign = "-";
+ break;
+ case "unknown":
+ sign = "";
+ }
+ return (
+ <Column
+ style={{
+ textAlign: "center",
+ color:
+ props.currentState !== TransactionMajorState.Done
+ ? "gray"
+ : sign === "+"
+ ? "darkgreen"
+ : sign === "-"
+ ? "darkred"
+ : undefined,
+ }}
+ >
+ <ExtraLargeText>
+ {sign}
+ {Amounts.stringifyValue(props.amount, 2)}
+ </ExtraLargeText>
+ {props.currentState === TransactionMajorState.Aborted ? (
+ <div
+ style={{
+ color: "black",
+ border: "1px black solid",
+ borderRadius: 8,
+ padding: 4,
+ }}
+ >
+ <i18n.Translate>ABORTED</i18n.Translate>
+ </div>
+ ) : props.currentState === TransactionMajorState.Failed ? (
+ <div
+ style={{
+ color: "red",
+ border: "1px darkred solid",
+ borderRadius: 8,
+ padding: 4,
+ }}
+ >
+ <i18n.Translate>FAILED</i18n.Translate>
+ </div>
+ ) : undefined}
+ </Column>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/Loading.tsx b/packages/taler-wallet-webextension/src/components/Loading.tsx
new file mode 100644
index 000000000..b0209f855
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Loading.tsx
@@ -0,0 +1,100 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { css } from "@linaria/core";
+import { Fragment, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import ProgressIcon from "../svg/progress.inline.svg";
+import { CenteredText } from "./styled/index.js";
+
+const fadeIn = css`
+ & {
+ animation: fadein 3s;
+ }
+ @keyframes fadein {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+ }
+`;
+
+export function Loading(): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <section style={{ margin: "auto" }}>
+ <CenteredText class={fadeIn}>
+ <i18n.Translate>Loading</i18n.Translate>...
+ </CenteredText>
+ {/* <div class={ripple} style={{ "--size": "250px" }}>
+ <div></div>
+ <div></div>
+ </div> */}
+ <div class={fadeIn} dangerouslySetInnerHTML={{ __html: ProgressIcon }} />
+ </section>
+ );
+}
+
+const ripple = css`
+ & {
+ display: inline-block;
+ position: relative;
+ width: var(--size);
+ height: var(--size);
+ }
+ & div {
+ position: absolute;
+ border: 4px solid black;
+ opacity: 1;
+ border-radius: 50%;
+ animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
+ }
+ & div:nth-child(2) {
+ animation-delay: -0.3s;
+ }
+ @keyframes lds-ripple {
+ 0% {
+ top: calc(var(--size) / 2);
+ left: calc(var(--size) / 2);
+ width: 0;
+ height: 0;
+ opacity: 0;
+ }
+ 14.9% {
+ top: calc(var(--size) / 2);
+ left: calc(var(--size) / 2);
+ width: 0;
+ height: 0;
+ opacity: 0;
+ }
+ 15% {
+ top: calc(var(--size) / 2);
+ left: calc(var(--size) / 2);
+ width: 0;
+ height: 0;
+ opacity: 1;
+ }
+ 100% {
+ top: 0px;
+ left: 0px;
+ width: var(--size);
+ height: var(--size);
+ opacity: 0;
+ }
+ }
+`;
diff --git a/packages/taler-wallet-webextension/src/components/LogoHeader.tsx b/packages/taler-wallet-webextension/src/components/LogoHeader.tsx
index 9b75c62a1..2330b1b95 100644
--- a/packages/taler-wallet-webextension/src/components/LogoHeader.tsx
+++ b/packages/taler-wallet-webextension/src/components/LogoHeader.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,18 +14,22 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { h } from "preact";
+import { h, VNode } from "preact";
+import logo from "../svg/logo-2021.inline.svg";
-export function LogoHeader() {
- return <div style={{
- display: 'flex',
- justifyContent: 'space-around',
- margin: '2em',
- }}>
- <img style={{
- width: 150,
- height: 70,
- }} src="/static/img/logo-2021.svg" width="150" />
- </div>
-
-} \ No newline at end of file
+export function LogoHeader(): VNode {
+ return (
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-around",
+ margin: "2em",
+ }}
+ >
+ <div
+ style={{ width: 150, height: 70 }}
+ dangerouslySetInnerHTML={{ __html: logo }}
+ ></div>
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/Modal.tsx b/packages/taler-wallet-webextension/src/components/Modal.tsx
new file mode 100644
index 000000000..f8c0f1651
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Modal.tsx
@@ -0,0 +1,95 @@
+/*
+ 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 { styled } from "@linaria/react";
+import { ComponentChildren, h, VNode } from "preact";
+import { ButtonHandler } from "../mui/handlers.js";
+import closeIcon from "../svg/close_24px.inline.svg";
+import { Link } from "./styled/index.js";
+
+interface Props {
+ children: ComponentChildren;
+ onClose: ButtonHandler;
+ title: string;
+}
+
+const FullSize = styled.div`
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ z-index: 10;
+`;
+
+const Header = styled.div`
+ display: flex;
+ justify-content: space-between;
+ height: 5%;
+ vertical-align: center;
+ align-items: center;
+`;
+
+const Body = styled.div`
+ height: 95%;
+`;
+
+export function Modal({ title, children, onClose }: Props): VNode {
+ return (
+ <div style={{ top: 0, width: "100%", height: "100%" }}>
+
+ <FullSize onClick={onClose?.onClick}>
+ <div
+ onClick={(e) => e.stopPropagation()}
+ style={{
+ background: "white",
+ width: 600,
+ height: "80%",
+ margin: "auto",
+ borderRadius: 8,
+ padding: 8,
+ zIndex: 100,
+ // overflow: "scroll",
+ }}
+ >
+ <Header>
+ <div>
+ <h2>{title}</h2>
+ </div>
+ <Link onClick={onClose?.onClick}>
+ <div
+ style={{
+ height: 24,
+ width: 24,
+ marginLeft: 4,
+ marginRight: 4,
+ // fill: "white",
+ }}
+ dangerouslySetInnerHTML={{ __html: closeIcon }}
+ />
+ </Link>
+ </Header>
+ <hr />
+
+ <Body onClick={(e: any) => e.stopPropagation()}>{children}</Body>
+ </div>
+ </FullSize>
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx b/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx
new file mode 100644
index 000000000..7d3cf3f57
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/MultiActionButton.tsx
@@ -0,0 +1,129 @@
+/*
+ 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 { TranslatedString } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Button } from "../mui/Button.js";
+import arrowDown from "../svg/chevron-down.inline.svg";
+import { ParagraphClickable } from "./styled/index.js";
+
+export interface Props {
+ label: (s: string) => TranslatedString;
+ actions: string[];
+ onClick: (s: string) => Promise<void>;
+}
+
+/**
+ * functionality: it will receive a list of actions, take the first actions as
+ * the first chosen action
+ * the user may change the chosen action
+ * when the user click the button it will call onClick with the chosen action
+ * as argument
+ *
+ * visually: it is a primary button with a select handler on the right
+ *
+ * @returns
+ */
+export function MultiActionButton({
+ label,
+ actions,
+ onClick: doClick,
+}: Props): VNode {
+ const defaultAction = actions.length > 0 ? actions[0] : "";
+
+ const [opened, setOpened] = useState(false);
+ const [selected, setSelected] = useState<string>(defaultAction);
+
+ const canChange = actions.length > 1;
+ const options = canChange ? actions.filter((a) => a !== selected) : [];
+ function select(m: string): void {
+ setSelected(m);
+ setOpened(false);
+ }
+
+ if (!canChange) {
+ return (
+ <Button variant="contained" onClick={() => doClick(selected)}>
+ {label(selected)}
+ </Button>
+ );
+ }
+
+ return (
+ <div style={{ position: "relative", display: "inline-block" }}>
+ {opened && (
+ <div
+ style={{
+ position: "absolute",
+ bottom: 32 + 5,
+ right: 0,
+ marginLeft: 8,
+ marginRight: 8,
+ borderRadius: 5,
+ border: "1px solid blue",
+ background: "white",
+ boxShadow: "0px 8px 16px 0px rgba(0,0,0,0.2)",
+ zIndex: 1,
+ }}
+ >
+ {options.map((m) => (
+ <ParagraphClickable key={m} onClick={() => select(m)}>
+ {label(m)}
+ </ParagraphClickable>
+ ))}
+ </div>
+ )}
+ <Button
+ variant="contained"
+ onClick={() => doClick(selected)}
+ style={{
+ borderTopRightRadius: 0,
+ borderBottomRightRadius: 0,
+ marginRight: 0,
+ // maxWidth: 170,
+ overflowX: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ {label(selected)}
+ </Button>
+
+ <Button
+ variant="outlined"
+ onClick={async () => setOpened((s) => !s)}
+ style={{
+ marginLeft: 0,
+ borderTopLeftRadius: 0,
+ borderBottomLeftRadius: 0,
+ paddingLeft: 4,
+ paddingRight: 4,
+ minWidth: "unset",
+ }}
+ >
+ <div
+ style={{
+ height: 24,
+ width: 24,
+ marginLeft: 4,
+ marginRight: 4,
+ // fill: "white",
+ }}
+ dangerouslySetInnerHTML={{ __html: arrowDown }}
+ />
+ </Button>
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/Part.tsx b/packages/taler-wallet-webextension/src/components/Part.tsx
index 75c9df16f..2fb03308b 100644
--- a/packages/taler-wallet-webextension/src/components/Part.tsx
+++ b/packages/taler-wallet-webextension/src/components/Part.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 Taler Systems SA
+ (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
@@ -13,20 +13,186 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountLike } from "@gnu-taler/taler-util";
-import { ExtraLargeText, LargeText, SmallLightText } from "./styled";
-import { h } from 'preact';
-export type Kind = 'positive' | 'negative' | 'neutral';
+import {
+ PaytoUri,
+ stringifyPaytoUri,
+ TranslatedString,
+} 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 arrowDown from "../svg/chevron-down.inline.svg";
+import {
+ ExtraLargeText,
+ LargeText,
+ SmallBoldText
+} from "./styled/index.js";
+
+export type Kind = "positive" | "negative" | "neutral";
interface Props {
- title: string, text: AmountLike, kind: Kind, big?: boolean
+ title: VNode | TranslatedString;
+ text: VNode | TranslatedString;
+ kind?: Kind;
+ big?: boolean;
+ showSign?: boolean;
+}
+export function Part({
+ text,
+ title,
+ kind = "neutral",
+ big,
+ showSign,
+}: Props): VNode {
+ const Text = big ? ExtraLargeText : LargeText;
+ return (
+ <div style={{ margin: "1em" }}>
+ <SmallBoldText style={{ marginBottom: "1em" }}>{title}</SmallBoldText>
+ <Text
+ style={{
+ color:
+ kind == "positive" ? "green" : kind == "negative" ? "red" : "black",
+ fontWeight: "lighten",
+ }}
+ >
+ {!showSign || kind === "neutral"
+ ? undefined
+ : kind === "positive"
+ ? "+"
+ : "-"}
+ {text}
+ </Text>
+ </div>
+ );
+}
+
+const CollasibleBox = styled.div`
+ border: 1px solid black;
+ border-radius: 0.25em;
+ display: flex;
+ vertical-align: middle;
+ justify-content: space-between;
+ flex-direction: column;
+ /* margin: 0.5em; */
+ padding: 0.5em;
+ /* margin: 1em; */
+ /* width: 100%; */
+ /* color: #721c24; */
+ /* background: #f8d7da; */
+
+ & > div {
+ display: flex;
+ justify-content: space-between;
+ div {
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+ & > button {
+ align-self: center;
+ font-size: 100%;
+ padding: 0;
+ height: 28px;
+ width: 28px;
+ }
+ }
+`;
+
+export function PartCollapsible({ text, title }: Props): VNode {
+ const [collapsed, setCollapsed] = useState(true);
+
+ return (
+ <CollasibleBox>
+ <div>
+ <SmallBoldText>{title}</SmallBoldText>
+ <button
+ onClick={() => {
+ setCollapsed((v) => !v);
+ }}
+ >
+ <div
+ style={{
+ transform: !collapsed ? "scaleY(-1)" : undefined,
+ height: 24,
+ }}
+ dangerouslySetInnerHTML={{ __html: arrowDown }}
+ />
+ </button>
+ </div>
+ {/* <SmallBoldText
+ style={{
+ paddingBottom: "1em",
+ paddingTop: "1em",
+ paddingLeft: "1em",
+ border: "black solid 1px",
+ }}
+ >
+
+ </SmallBoldText> */}
+ {!collapsed && <div style={{ display: "block" }}>{text}</div>}
+ </CollasibleBox>
+ );
+}
+
+interface PropsPayto {
+ payto: PaytoUri;
+ kind: Kind;
+ big?: boolean;
}
-export function Part({ text, title, kind, big }: Props) {
+export function PartPayto({ payto, kind, big }: PropsPayto): VNode {
const Text = big ? ExtraLargeText : LargeText;
- return <div style={{ margin: '1em' }}>
- <SmallLightText style={{ margin: '.5em' }}>{title}</SmallLightText>
- <Text style={{ color: kind == 'positive' ? 'green' : (kind == 'negative' ? 'red' : 'black') }}>
- {text}
- </Text>
- </div>
+ let text: VNode | undefined = undefined;
+ let title = "";
+ const { i18n } = useTranslationContext();
+ if (payto.isKnown) {
+ if (payto.targetType === "x-taler-bank") {
+ text = (
+ <a target="_bank" rel="noreferrer" href={payto.host}>
+ {payto.account}
+ </a>
+ );
+ title = i18n.str`Bank account`;
+ } else if (payto.targetType === "bitcoin") {
+ text =
+ payto.segwitAddrs && payto.segwitAddrs.length > 0 ? (
+ <ul>
+ <li>{payto.targetPath}</li>
+ <li>{payto.segwitAddrs[0]}</li>
+ <li>{payto.segwitAddrs[1]}</li>
+ </ul>
+ ) : (
+ <Fragment>{payto.targetPath}</Fragment>
+ );
+ title = i18n.str`Bitcoin address`;
+ } else if (payto.targetType === "iban") {
+ if (payto.bic) {
+ text = (
+ <Fragment>
+ {payto.bic}/{payto.iban}
+ </Fragment>
+ );
+ title = i18n.str`BIC/IBAN`;
+ } else {
+ text = <Fragment>{payto.iban}</Fragment>;
+ title = i18n.str`IBAN`;
+ }
+ }
+ }
+ if (!text) {
+ text = <Fragment>{stringifyPaytoUri(payto)}</Fragment>;
+ title = "Payto URI";
+ }
+ return (
+ <div style={{ margin: "1em" }}>
+ <SmallBoldText>{title}</SmallBoldText>
+ <Text
+ style={{
+ color:
+ kind == "positive" ? "green" : kind == "negative" ? "red" : "black",
+ }}
+ >
+ {text}
+ </Text>
+ </div>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
new file mode 100644
index 000000000..7fa0376c9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
@@ -0,0 +1,239 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AmountJson,
+ Amounts,
+ PaymentInsufficientBalanceDetails,
+ PreparePayResult,
+ PreparePayResultType,
+ TranslatedString,
+ parsePayUri,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { Button } from "../mui/Button.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { assertUnreachable } from "../utils/index.js";
+import { Amount } from "./Amount.js";
+import { Part } from "./Part.js";
+import { QR } from "./QR.js";
+import { LinkSuccess, WarningBox } from "./styled/index.js";
+
+interface Props {
+ payStatus: PreparePayResult;
+ payHandler: ButtonHandler | undefined;
+ uri: string;
+ amount: AmountJson;
+ goToWalletManualWithdraw: (currency: string) => Promise<void>;
+}
+
+export function PaymentButtons({
+ payStatus,
+ uri,
+ payHandler,
+ amount,
+ goToWalletManualWithdraw,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ if (payStatus.status === PreparePayResultType.PaymentPossible) {
+ return (
+ <Fragment>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={payHandler?.onClick}
+ >
+ <i18n.Translate>
+ Pay &nbsp;
+ {<Amount value={amount} />}
+ </i18n.Translate>
+ </Button>
+ </section>
+ <PayWithMobile uri={uri} />
+ </Fragment>
+ );
+ }
+
+ if (payStatus.status === PreparePayResultType.InsufficientBalance) {
+ const reason = getReason(payStatus.balanceDetails);
+
+ let BalanceMessage = "";
+ switch (reason) {
+ case "age-acceptable": {
+ BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue(
+ payStatus.balanceDetails.balanceAgeAcceptable,
+ )} ${amount.currency} to pay for this contract which is restricted.`;
+ break;
+ }
+ case "available": {
+ BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue(
+ payStatus.balanceDetails.balanceAvailable,
+ )} ${amount.currency} available.`;
+ break;
+ }
+ case "merchant-acceptable": {
+ BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue(
+ payStatus.balanceDetails.balanceReceiverAcceptable,
+ )} ${amount.currency
+ } . To know more you can check which exchange and auditors the merchant trust.`;
+ break;
+ }
+ case "merchant-depositable": {
+ BalanceMessage = i18n.str`Balance is not enough because merchant will just accept ${Amounts.stringifyValue(
+ payStatus.balanceDetails.balanceReceiverDepositable,
+ )} ${amount.currency
+ } . To know more you can check which wire methods the merchant accepts.`;
+ break;
+ }
+ case "material": {
+ BalanceMessage = i18n.str`Balance is not enough because you have ${Amounts.stringifyValue(
+ payStatus.balanceDetails.balanceMaterial,
+ )} ${amount.currency
+ } to spend right know. There are some coins that need to be refreshed.`;
+ break;
+ }
+ case "fee-gap": {
+ BalanceMessage = i18n.str`Balance looks like it should be enough, but doesn't cover all fees requested by the merchant and payment processor. Please ensure there is at least ${Amounts.stringifyValue(
+ Amounts.stringify(
+ Amounts.sub(
+ amount,
+ payStatus.balanceDetails.maxEffectiveSpendAmount,
+ ).amount,
+ ),
+ )} ${amount.currency
+ } more balance in your wallet or ask your merchant to cover more of the fees.`;
+ break;
+ }
+ default:
+ assertUnreachable(reason);
+ }
+
+ return (
+ <Fragment>
+ <section>
+ <WarningBox>{BalanceMessage}</WarningBox>
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))}
+ >
+ <i18n.Translate>Get digital cash</i18n.Translate>
+ </Button>
+ </section>
+ <PayWithMobile uri={uri} />
+ </Fragment>
+ );
+ }
+ if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
+ return (
+ <Fragment>
+ <section>
+ {payStatus.paid && payStatus.contractTerms.fulfillment_message && (
+ <Part
+ title={i18n.str`Merchant message`}
+ text={
+ payStatus.contractTerms.fulfillment_message as TranslatedString
+ }
+ kind="neutral"
+ />
+ )}
+ </section>
+ </Fragment>
+ );
+ }
+
+ assertUnreachable(payStatus);
+}
+
+function PayWithMobile({ uri }: { uri: string }): VNode {
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+
+ const payUri = parsePayUri(uri);
+
+ const [showQR, setShowQR] = useState<string | undefined>(undefined);
+ async function sharePrivatePaymentURI() {
+ if (!payUri) {
+ return;
+ }
+ if (!showQR) {
+ const result = await api.wallet.call(WalletApiOperation.SharePayment, {
+ merchantBaseUrl: payUri.merchantBaseUrl,
+ orderId: payUri.orderId,
+ });
+ setShowQR(result.privatePayUri);
+ } else {
+ setShowQR(undefined);
+ }
+ }
+ if (!payUri) {
+ return <Fragment />
+ }
+ return (
+ <section>
+ <LinkSuccess upperCased onClick={sharePrivatePaymentURI}>
+ {!showQR ? i18n.str`Pay with a mobile phone` : i18n.str`Hide QR`}
+ </LinkSuccess>
+ {showQR && (
+ <div>
+ <QR text={showQR} />
+ <i18n.Translate>
+ Scan the QR code or &nbsp;
+ <a href={showQR}>
+ <i18n.Translate>click here</i18n.Translate>
+ </a>
+ </i18n.Translate>
+ </div>
+ )}
+ </section>
+ );
+}
+
+type NoEnoughBalanceReason =
+ | "available"
+ | "material"
+ | "age-acceptable"
+ | "merchant-acceptable"
+ | "merchant-depositable"
+ | "fee-gap";
+
+function getReason(
+ info: PaymentInsufficientBalanceDetails,
+): NoEnoughBalanceReason {
+ if (Amounts.cmp(info.amountRequested, info.balanceAvailable) > 0) {
+ return "available";
+ }
+ if (Amounts.cmp(info.amountRequested, info.balanceMaterial) > 0) {
+ return "material";
+ }
+ if (Amounts.cmp(info.amountRequested, info.balanceAgeAcceptable) > 0) {
+ return "age-acceptable";
+ }
+ if (Amounts.cmp(info.amountRequested, info.balanceReceiverAcceptable) > 0) {
+ return "merchant-acceptable";
+ }
+ if (Amounts.cmp(info.amountRequested, info.balanceReceiverDepositable) > 0) {
+ return "merchant-depositable";
+ }
+ return "fee-gap";
+}
diff --git a/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx b/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx
new file mode 100644
index 000000000..d1c49aea2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/PendingTransactions.stories.tsx
@@ -0,0 +1,118 @@
+/*
+ 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 {
+ TalerProtocolTimestamp,
+ Transaction,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { PendingTransactionsView as TestedComponent } from "./PendingTransactions.js";
+
+export default {
+ title: "PendingTransactions",
+ component: TestedComponent,
+};
+
+export const OnePendingTransaction = tests.createExample(TestedComponent, {
+ transactions: [
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ ],
+});
+
+export const ThreePendingTransactions = tests.createExample(TestedComponent, {
+ transactions: [
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ ],
+});
+
+export const TenPendingTransactions = tests.createExample(TestedComponent, {
+ transactions: [
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ {
+ amountEffective: "USD:10",
+ type: TransactionType.Withdrawal,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1),
+ } as Transaction,
+ ],
+});
diff --git a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
new file mode 100644
index 000000000..c94010ede
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
@@ -0,0 +1,205 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ Amounts,
+ NotificationType,
+ Transaction,
+ TransactionMajorState,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { Fragment, h, JSX, VNode } from "preact";
+import { useEffect } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { Avatar } from "../mui/Avatar.js";
+import { Grid } from "../mui/Grid.js";
+import { Typography } from "../mui/Typography.js";
+import Banner from "./Banner.js";
+import { Time } from "./Time.js";
+
+interface Props extends JSX.HTMLAttributes {
+ goToTransaction?: (id: string) => Promise<void>;
+ goToURL: (url: string) => void;
+}
+
+/**
+ * this cache will save the tx from the previous render
+ */
+const cache = { tx: [] as Transaction[] };
+
+export function PendingTransactions({
+ goToTransaction,
+ goToURL,
+}: Props): VNode {
+ const api = useBackendContext();
+ const state = useAsyncAsHook(() =>
+ api.wallet.call(WalletApiOperation.GetTransactions, {}),
+ );
+
+ useEffect(() => {
+ return api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
+ state?.retry,
+ );
+ });
+
+ const transactions =
+ !state || state.hasError
+ ? cache.tx
+ : state.response.transactions.filter(
+ (t) => t.txState.major === TransactionMajorState.Pending,
+ );
+
+ if (state && !state.hasError) {
+ cache.tx = transactions;
+ }
+ if (!transactions.length) {
+ return <Fragment />;
+ }
+ return (
+ <PendingTransactionsView
+ goToTransaction={goToTransaction}
+ goToURL={goToURL}
+ transactions={transactions}
+ />
+ );
+}
+
+export function PendingTransactionsView({
+ transactions,
+ goToTransaction,
+ goToURL,
+}: {
+ goToTransaction?: (id: string) => Promise<void>;
+ goToURL: (id: string) => void;
+ transactions: Transaction[];
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const kycTransaction = transactions.find((tx) => tx.kycUrl);
+ if (kycTransaction) {
+ return (
+ <div
+ style={{
+ backgroundColor: "#fff3cd",
+ color: "#664d03",
+ display: "flex",
+ justifyContent: "center",
+ }}
+ >
+ <Banner
+ titleHead={i18n.str`KYC requirement`}
+ style={{
+ backgroundColor: "lightred",
+ maxHeight: 150,
+ padding: 8,
+ flexGrow: 1, //#fff3cd //#ffecb5
+ maxWidth: 500,
+ overflowY: transactions.length > 3 ? "scroll" : "hidden",
+ }}
+ >
+ <Grid
+ container
+ item
+ xs={1}
+ wrap="nowrap"
+ role="button"
+ spacing={1}
+ alignItems="center"
+ onClick={() => {
+ goToURL(kycTransaction.kycUrl ?? "#");
+ }}
+ >
+ <Grid item>
+ <Typography inline bold>
+ One or more transaction require a KYC step to complete
+ </Typography>
+ </Grid>
+ </Grid>
+ </Banner>
+ </div>
+ );
+ }
+
+ if (!goToTransaction) return <Fragment />;
+
+ return (
+ <div
+ style={{
+ backgroundColor: "lightcyan",
+ display: "flex",
+ justifyContent: "center",
+ }}
+ >
+ <Banner
+ titleHead={i18n.str`PENDING OPERATIONS`}
+ style={{
+ backgroundColor: "lightcyan",
+ maxHeight: 150,
+ padding: 8,
+ flexGrow: 1,
+ maxWidth: 500,
+ overflowY: transactions.length > 3 ? "scroll" : "hidden",
+ }}
+ >
+ {transactions.map((t, i) => {
+ const amount = Amounts.parseOrThrow(t.amountEffective);
+ return (
+ <Grid
+ container
+ item
+ xs={1}
+ key={i}
+ wrap="nowrap"
+ role="button"
+ spacing={1}
+ alignItems="center"
+ onClick={() => {
+ goToTransaction(t.transactionId);
+ }}
+ >
+ <Grid item xs={"auto"}>
+ <Avatar
+ style={{
+ border: "solid blue 1px",
+ color: "blue",
+ boxSizing: "border-box",
+ }}
+ >
+ {t.type.substring(0, 1)}
+ </Avatar>
+ </Grid>
+
+ <Grid item>
+ <Typography inline bold>
+ {amount.currency} {Amounts.stringifyValue(amount)}
+ </Typography>
+ &nbsp;-&nbsp;
+ <Time
+ timestamp={AbsoluteTime.fromPreciseTimestamp(t.timestamp)}
+ format="dd MMMM yyyy"
+ />
+ </Grid>
+ </Grid>
+ );
+ })}
+ </Banner>
+ </div>
+ );
+}
+
+export default PendingTransactions;
diff --git a/packages/taler-wallet-webextension/src/components/ProductList.tsx b/packages/taler-wallet-webextension/src/components/ProductList.tsx
new file mode 100644
index 000000000..748935dff
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/ProductList.tsx
@@ -0,0 +1,89 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts, Product } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { SmallLightText } from "./styled/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+
+export function ProductList({ products }: { products: Product[] }): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <SmallLightText style={{ margin: ".5em" }}>
+ <i18n.Translate>List of products</i18n.Translate>
+ </SmallLightText>
+ <dl>
+ {products.map((p, i) => {
+ if (p.price) {
+ const pPrice = Amounts.parseOrThrow(p.price);
+ return (
+ <div key={i} style={{ display: "flex", textAlign: "left" }}>
+ <div>
+ <img
+ src={p.image ? p.image : undefined}
+ style={{ width: 32, height: 32 }}
+ />
+ </div>
+ <div>
+ <dt>
+ {p.quantity ?? 1} x {p.description}{" "}
+ <span style={{ color: "gray" }}>
+ {Amounts.stringify(pPrice)}
+ </span>
+ </dt>
+ <dd>
+ <b>
+ {Amounts.stringify(
+ Amounts.mult(pPrice, p.quantity ?? 1).amount,
+ )}
+ </b>
+ </dd>
+ </div>
+ </div>
+ );
+ }
+ return (
+ <div key={i} style={{ display: "flex", textAlign: "left" }}>
+ <div>
+ <img src={p.image} style={{ width: 32, height: 32 }} />
+ </div>
+ <div>
+ <dt>
+ {p.quantity ?? 1} x {p.description}
+ </dt>
+ <dd>
+ <i18n.Translate>Total</i18n.Translate>
+ {` `}
+ {p.price ? (
+ `${Amounts.stringifyValue(
+ Amounts.mult(
+ Amounts.parseOrThrow(p.price),
+ p.quantity ?? 1,
+ ).amount,
+ )} ${p}`
+ ) : (
+ <i18n.Translate>free</i18n.Translate>
+ )}
+ </dd>
+ </div>
+ </div>
+ );
+ })}
+ </dl>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx b/packages/taler-wallet-webextension/src/components/QR.stories.tsx
index cd443e9d4..1d1f15b69 100644
--- a/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/QR.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,30 +15,17 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { NavBar as TestedComponent } from '../NavigationBar';
+import * as tests from "@gnu-taler/web-util/testing";
+import { QR } from "./QR.js";
export default {
- title: 'popup/header',
- // component: TestedComponent,
- argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
+ title: "qr",
};
-
-export const OnBalance = createExample(TestedComponent, {
- devMode:false,
- path:'/balance'
-});
-
-export const OnHistoryWithDevMode = createExample(TestedComponent, {
- devMode:true,
- path:'/history'
+export const Restore = tests.createExample(QR, {
+ text: "taler://restore/6J0RZTJC6AV21WXK87BTE67WTHE9P2QSHF2BZXTP7PDZY2ARYBPG@sync1.demo.taler.net,sync2.demo.taler.net,sync1.demo.taler.net,sync3.demo.taler.net",
});
diff --git a/packages/taler-wallet-webextension/src/components/QR.tsx b/packages/taler-wallet-webextension/src/components/QR.tsx
index 8e3f69295..60710ab15 100644
--- a/packages/taler-wallet-webextension/src/components/QR.tsx
+++ b/packages/taler-wallet-webextension/src/components/QR.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,24 +14,35 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- import { h, VNode } from "preact";
- import { useEffect, useRef } from "preact/hooks";
- import qrcode from "qrcode-generator";
-
- export function QR({ text }: { text: string; }):VNode {
- const divRef = useRef<HTMLDivElement>(null);
- useEffect(() => {
- if (!divRef.current) return
- const qr = qrcode(0, 'L');
- qr.addData(text);
- qr.make();
- divRef.current.innerHTML = qr.createSvgTag({
- scalable: true,
- });
- });
-
- return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
- <div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} />
- </div>;
- }
- \ No newline at end of file
+import { h, VNode } from "preact";
+import { useEffect, useRef } from "preact/hooks";
+import qrcode from "qrcode-generator";
+
+export function QR({ text }: { text: string }): VNode {
+ const divRef = useRef<HTMLDivElement>(null);
+ useEffect(() => {
+ if (!divRef.current) return;
+ const qr = qrcode(0, "H");
+ qr.addData(text);
+ qr.make();
+ divRef.current.innerHTML = qr.createSvgTag({
+ scalable: true,
+ });
+ });
+
+ return (
+ <div
+ style={{
+ width: "100%",
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ }}
+ >
+ <div
+ style={{ width: "50%", minWidth: 200, maxWidth: 300 }}
+ ref={divRef}
+ />
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/SelectList.tsx b/packages/taler-wallet-webextension/src/components/SelectList.tsx
index 536e5b89a..6eb72a266 100644
--- a/packages/taler-wallet-webextension/src/components/SelectList.tsx
+++ b/packages/taler-wallet-webextension/src/components/SelectList.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 Taler Systems SA
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,55 +14,80 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { JSX } from "preact/jsx-runtime";
-import { NiceSelect } from "./styled/index";
-import { h } from "preact";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { NiceSelect } from "./styled/index.js";
interface Props {
value?: string;
- onChange: (s: string) => void;
- label: string;
+ onChange?: (s: string) => void;
+ label: VNode | TranslatedString;
list: {
- [label: string]: string
- }
+ [label: string]: string;
+ };
name: string;
description?: string;
canBeNull?: boolean;
+ maxWidth?: boolean;
}
-export function SelectList({ name, value, list, canBeNull, onChange, label, description }: Props): JSX.Element {
- return <div>
- <label
- htmlFor={`text-${name}`}
- style={{ marginLeft: "0.5em", fontWeight: "bold" }}
- > {label}</label>
- <NiceSelect>
- <select name={name} onChange={(e) => {
- console.log(e.currentTarget.value, value)
- onChange(e.currentTarget.value)
- }}>
- {value !== undefined ? <option selected>
- {list[value]}
- </option> : <option selected disabled>
- Select one option
- </option>}
- {Object.keys(list)
- .filter((l) => l !== value)
- .map(key => <option value={key} key={key}>{list[key]}</option>)
- }
- </select>
- </NiceSelect>
- {description && <span
- style={{
- color: "#383838",
- fontSize: "smaller",
- display: "block",
- marginLeft: "2em",
- }}
- >
- {description}
- </span>}
-
- </div>
-
+export function SelectList({
+ name,
+ value,
+ list,
+ onChange,
+ label,
+ maxWidth,
+ description,
+ canBeNull,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <label
+ htmlFor={`text-${name}`}
+ style={{ marginLeft: "0.5em", fontWeight: "bold" }}
+ >
+ {" "}
+ {label}
+ </label>
+ <NiceSelect>
+ <select
+ name={name}
+ value={value}
+ style={maxWidth ? { width: "100%" } : undefined}
+ onChange={(e) => {
+ if (onChange) onChange(e.currentTarget.value);
+ }}
+ >
+ {value === undefined ||
+ (canBeNull && (
+ <option selected disabled>
+ <i18n.Translate>Select one option</i18n.Translate>
+ </option>
+ ))}
+ {Object.keys(list)
+ // .filter((l) => l !== value)
+ .map((key) => (
+ <option value={key} key={key}>
+ {list[key]}
+ </option>
+ ))}
+ </select>
+ </NiceSelect>
+ {description && (
+ <span
+ style={{
+ color: "#383838",
+ fontSize: "smaller",
+ display: "block",
+ marginLeft: "2em",
+ }}
+ >
+ {description}
+ </span>
+ )}
+ </Fragment>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
new file mode 100644
index 000000000..0e23d5850
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
@@ -0,0 +1,98 @@
+/*
+ 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 {
+ ErrorView,
+ HiddenView,
+ LoadingView,
+ ShowView,
+} from "./ShowFullContractTermPopup.js";
+import { AmountString, WalletContractData } from "@gnu-taler/taler-util";
+
+export default {
+ title: "ShowFullContractTermPopup",
+};
+
+const cd: WalletContractData = {
+ amount: "ARS:2" as AmountString,
+ contractTermsHash:
+ "92X0KSJPZ8XS2XECCGFWTCGW8XMFCXTT2S6WHZDP6H9Y3TSKMTHY94WXEWDERTNN5XWCYGW4VN5CF2D4846HXTW7P06J4CZMHCWKC9G",
+ fulfillmentUrl: "",
+ merchantBaseUrl: "https://merchant-backend.taler.ar/",
+ merchantPub: "JZYHJ13M91GMSQMT75J8Q6ZN0QP8XF8CRHR7K5MMWYE8JQB6AAPG",
+ merchantSig:
+ "0YA1WETV15R6K8QKS79QA3QMT16010F42Q49VSKYQ71HVQKAG0A4ZJCA4YTKHE9EA5SP156TJSKZEJJJ87305N6PS80PC48RNKYZE08",
+ orderId: "2022.220-0281XKKB8W7YE",
+ summary: "w",
+ payDeadline: {
+ t_s: 1660002673,
+ },
+ refundDeadline: {
+ t_s: 1660002673,
+ },
+ allowedExchanges: [
+ {
+ exchangeBaseUrl: "https://exchange.taler.ar/",
+ exchangePub: "1C2EYE90PYDNVRTQ25A3PA0KW5W4WPAJNNQHVHV49PT6W5CERFV0",
+ },
+ ],
+ timestamp: {
+ t_s: 1659972710,
+ },
+ wireMethod: "x-taler-bank",
+ wireInfoHash:
+ "QDT28374ZHYJ59WQFZ3TW1D5WKJVDYHQT86VHED3TNMB15ANJSKXDYPPNX01348KDYCX6T4WXA5A8FJJ8YWNEB1JW726C1JPKHM89DR",
+ maxDepositFee: "ARS:1" as AmountString,
+ merchant: {
+ name: "Default",
+ address: {
+ country: "ar",
+ },
+ jurisdiction: {
+ country: "ar",
+ },
+ },
+ // products: [],
+ autoRefund: undefined,
+ summaryI18n: undefined,
+ // deliveryDate: undefined,
+ // deliveryLocation: undefined,
+};
+
+export const ShowingSimpleOrder = tests.createExample(ShowView, {
+ contractTerms: cd,
+});
+export const Error = tests.createExample(ErrorView, {
+ transactionId: "asd",
+ error: {
+ hasError: true,
+ message: "message",
+ // details: {
+ // co
+ // },
+ type: "error",
+ // details: {
+ // code: 123,
+ // },
+ },
+});
+export const Loading = tests.createExample(LoadingView, {});
+export const Hidden = tests.createExample(HiddenView, {});
diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
new file mode 100644
index 000000000..e655def39
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
@@ -0,0 +1,413 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ Duration,
+ Location,
+ TransactionIdStr,
+ WalletContractData,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { styled } from "@linaria/react";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../components/Loading.js";
+import { Modal } from "../components/Modal.js";
+import { Time } from "../components/Time.js";
+import { alertFromError, useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { compose, StateViewMap } from "../utils/index.js";
+import { Amount } from "./Amount.js";
+import { ErrorAlertView } from "./CurrentAlerts.js";
+import { Link } from "./styled/index.js";
+
+const ContractTermsTable = styled.table`
+ width: 100%;
+ border-spacing: 0px;
+ & > tr > td {
+ padding: 5px;
+ }
+ & > tr > td:nth-child(2n) {
+ text-align: right;
+ overflow-wrap: anywhere;
+ }
+ & > tr:nth-child(2n) {
+ background: #ebebeb;
+ }
+`;
+
+function locationAsText(l: Location | undefined): VNode {
+ if (!l) return <span />;
+ const lines = [
+ ...(l.address_lines || []).map((e) => [e]),
+ [l.town_location, l.town, l.street],
+ [l.building_name, l.building_number],
+ [l.country, l.country_subdivision],
+ [l.district, l.post_code],
+ ];
+ //remove all missing value
+ //then remove all empty lines
+ const curated = lines
+ .map((l) => l.filter((v) => !!v))
+ .filter((l) => l.length > 0);
+ return (
+ <span>
+ {curated.map((c, i) => (
+ <div key={i}>{c.join(",")}</div>
+ ))}
+ </span>
+ );
+}
+
+type State = States.Loading | States.Error | States.Hidden | States.Show;
+
+export namespace States {
+ export interface Loading {
+ status: "loading";
+ hideHandler: ButtonHandler;
+ }
+ export interface Error {
+ status: "error";
+ transactionId: string;
+ error: HookError;
+ hideHandler: ButtonHandler;
+ }
+ export interface Hidden {
+ status: "hidden";
+ showHandler: ButtonHandler;
+ }
+ export interface Show {
+ status: "show";
+ hideHandler: ButtonHandler;
+ contractTerms: WalletContractData;
+ }
+}
+
+interface Props {
+ transactionId: TransactionIdStr;
+}
+
+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, {
+ transactionId,
+ });
+ }, [show]);
+
+ const hideHandler = {
+ onClick: pushAlertOnError(async () => setShow(false)),
+ };
+ const showHandler = {
+ onClick: pushAlertOnError(async () => setShow(true)),
+ };
+ if (!show) {
+ return {
+ status: "hidden",
+ showHandler,
+ };
+ }
+ if (!hook) return { status: "loading", hideHandler };
+ if (hook.hasError)
+ return { status: "error", transactionId, error: hook, hideHandler };
+ if (!hook.response) return { status: "loading", hideHandler };
+ return {
+ status: "show",
+ contractTerms: hook.response,
+ hideHandler,
+ };
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: LoadingView,
+ error: ErrorView,
+ show: ShowView,
+ hidden: HiddenView,
+};
+
+export const ShowFullContractTermPopup = compose(
+ "ShowFullContractTermPopup",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
+
+export function LoadingView({ hideHandler }: States.Loading): VNode {
+ return (
+ <Modal title="Full detail" onClose={hideHandler}>
+ <Loading />
+ </Modal>
+ );
+}
+
+export function ErrorView({
+ hideHandler,
+ error,
+ transactionId,
+}: States.Error): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Modal title="Full detail" onClose={hideHandler}>
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not load purchase proposal details`,
+ error,
+ { transactionId },
+ )}
+ />
+ </Modal>
+ );
+}
+
+export function HiddenView({ showHandler }: States.Hidden): VNode {
+ return <Link onClick={showHandler?.onClick}>Show full details</Link>;
+}
+
+export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
+ const createdAt = AbsoluteTime.fromProtocolTimestamp(contractTerms.timestamp);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Modal title="Full detail" onClose={hideHandler}>
+ <div style={{ overflowY: "auto", height: "95%", padding: 5 }}>
+ <ContractTermsTable>
+ <tr>
+ <td>
+ <i18n.Translate>Order Id</i18n.Translate>
+ </td>
+ <td>{contractTerms.orderId}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Summary</i18n.Translate>
+ </td>
+ <td>{contractTerms.summary}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Amount</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={contractTerms.amount} />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Merchant name</i18n.Translate>
+ </td>
+ <td>{contractTerms.merchant.name}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Merchant jurisdiction</i18n.Translate>
+ </td>
+ <td>{locationAsText(contractTerms.merchant.jurisdiction)}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Merchant address</i18n.Translate>
+ </td>
+ <td>{locationAsText(contractTerms.merchant.address)}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Merchant logo</i18n.Translate>
+ </td>
+ <td>
+ <div>
+ <img
+ src={contractTerms.merchant.logo}
+ style={{ width: 64, height: 64, margin: 4 }}
+ />
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Merchant website</i18n.Translate>
+ </td>
+ <td>{contractTerms.merchant.website}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Merchant email</i18n.Translate>
+ </td>
+ <td>{contractTerms.merchant.email}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Merchant public key</i18n.Translate>
+ </td>
+ <td>
+ <span title={contractTerms.merchantPub}>
+ {contractTerms.merchantPub.substring(0, 6)}...
+ </span>
+ </td>
+ </tr>
+ {/* <tr>
+ <td>
+ <i18n.Translate>Delivery date</i18n.Translate>
+ </td>
+ <td>
+ {contractTerms.deliveryDate && (
+ <Time
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ contractTerms.deliveryDate,
+ )}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ )}
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Delivery location</i18n.Translate>
+ </td>
+ <td>{locationAsText(contractTerms.deliveryLocation)}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Products</i18n.Translate>
+ </td>
+ <td>
+ {!contractTerms.products || contractTerms.products.length === 0
+ ? "none"
+ : contractTerms.products
+ .map((p) => `${p.description} x ${p.quantity}`)
+ .join(", ")}
+ </td>
+ </tr> */}
+ <tr>
+ <td>
+ <i18n.Translate>Created at</i18n.Translate>
+ </td>
+ <td>
+ {contractTerms.timestamp && (
+ <Time
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ contractTerms.timestamp,
+ )}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ )}
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Refund deadline</i18n.Translate>
+ </td>
+ <td>
+ {
+ <Time
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ contractTerms.refundDeadline,
+ )}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ }
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Auto refund</i18n.Translate>
+ </td>
+ <td>
+ {
+ <Time
+ timestamp={AbsoluteTime.addDuration(
+ createdAt,
+ !contractTerms.autoRefund
+ ? Duration.getZero()
+ : Duration.fromTalerProtocolDuration(
+ contractTerms.autoRefund,
+ ),
+ )}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ }
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Pay deadline</i18n.Translate>
+ </td>
+ <td>
+ {
+ <Time
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ contractTerms.payDeadline,
+ )}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ }
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Fulfillment URL</i18n.Translate>
+ </td>
+ <td>{contractTerms.fulfillmentUrl}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Fulfillment message</i18n.Translate>
+ </td>
+ <td>{contractTerms.fulfillmentMessage}</td>
+ </tr>
+ {/* <tr>
+ <td>Public reorder URL</td>
+ <td>{contractTerms.public_reorder_url}</td>
+ </tr> */}
+ <tr>
+ <td>
+ <i18n.Translate>Max deposit fee</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={contractTerms.maxDepositFee} />
+ </td>
+ </tr>
+ {/* <tr>
+ <td>Extra</td>
+ <td>
+ <pre>{contractTerms.}</pre>
+ </td>
+ </tr> */}
+ <tr>
+ <td>
+ <i18n.Translate>Exchanges</i18n.Translate>
+ </td>
+ <td>
+ {(contractTerms.allowedExchanges || []).map((e) => (
+ <Fragment key={e.exchangePub}>
+ <a href={e.exchangeBaseUrl} title={e.exchangePub}>
+ {e.exchangePub.substring(0, 6)}...
+ </a>
+ &nbsp;
+ </Fragment>
+ ))}
+ </td>
+ </tr>
+ </ContractTermsTable>
+ </div>
+ </Modal>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
new file mode 100644
index 000000000..1585e3992
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/index.ts
@@ -0,0 +1,91 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ComponentChildren } from "preact";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { SelectFieldHandler, ToggleHandler } from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
+import { ErrorAlertView } from "../CurrentAlerts.js";
+import { useComponentState } from "./state.js";
+import { TermsState } from "./utils.js";
+import {
+ ShowButtonsAcceptedTosView,
+ ShowButtonsNonAcceptedTosView,
+ ShowTosContentView,
+} from "./views.js";
+
+export interface Props {
+ exchangeUrl: string;
+ readOnly?: boolean;
+ showEvenIfaccepted?: boolean;
+ children: ComponentChildren;
+}
+
+export type State =
+ | State.Loading
+ | State.Error
+ | State.ShowButtonsAccepted
+ | State.ShowButtonsNotAccepted
+ | State.ShowContent;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface Error {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ terms: TermsState;
+ }
+ export interface ShowContent extends BaseInfo {
+ status: "show-content";
+ termsAccepted: ToggleHandler;
+ showingTermsOfService?: ToggleHandler;
+ tosLang: SelectFieldHandler;
+ tosFormat: SelectFieldHandler;
+ }
+ export interface ShowButtonsAccepted extends BaseInfo {
+ status: "show-buttons-accepted";
+ termsAccepted: ToggleHandler;
+ showingTermsOfService: ToggleHandler;
+ children: ComponentChildren,
+ }
+ export interface ShowButtonsNotAccepted extends BaseInfo {
+ status: "show-buttons-not-accepted";
+ showingTermsOfService: ToggleHandler;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ "show-content": ShowTosContentView,
+ "show-buttons-accepted": ShowButtonsAcceptedTosView,
+ "show-buttons-not-accepted": ShowButtonsNonAcceptedTosView,
+};
+
+export const TermsOfService = compose(
+ "TermsOfService",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
new file mode 100644
index 000000000..76524f0f4
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/state.ts
@@ -0,0 +1,160 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { Props, State } from "./index.js";
+import { buildTermsOfServiceState } from "./utils.js";
+
+const supportedFormats = {
+ "text/html": "HTML",
+ "text/xml" : "XML",
+ "text/markdown" : "Markdown",
+ "text/plain" : "Plain text",
+ "text/pdf" : "PDF",
+}
+
+export function useComponentState({ showEvenIfaccepted, exchangeUrl, readOnly, children }: Props): State {
+ const api = useBackendContext();
+ const [showContent, setShowContent] = useState<boolean>(!!readOnly);
+ const { i18n, lang } = useTranslationContext();
+ const [tosLang, setTosLang] = useState<string>()
+ const { pushAlertOnError } = useAlertContext();
+
+ const [format, setFormat] = useState("text/html")
+
+ const acceptedLang = tosLang ?? lang
+ /**
+ * For the exchange selected, bring the status of the terms of service
+ */
+ const terms = useAsyncAsHook(async () => {
+ const exchangeTos = await api.wallet.call(
+ WalletApiOperation.GetExchangeTos,
+ {
+ exchangeBaseUrl: exchangeUrl,
+ acceptedFormat: [format],
+ acceptLanguage: acceptedLang,
+ },
+ );
+
+ const supportedLangs = exchangeTos.tosAvailableLanguages.reduce((prev, cur) => {
+ prev[cur] = cur
+ return prev;
+ }, {} as Record<string, string>)
+
+ const state = buildTermsOfServiceState(exchangeTos);
+
+ return { state, supportedLangs };
+ }, [acceptedLang, format]);
+
+ if (!terms) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (terms.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the status of the term of service`,
+ terms,
+ ),
+ };
+ }
+ const { state, supportedLangs } = terms.response;
+
+ async function onUpdate(accepted: boolean): Promise<void> {
+ if (!state) return;
+
+ if (accepted) {
+ await api.wallet.call(WalletApiOperation.SetExchangeTosAccepted, {
+ exchangeBaseUrl: exchangeUrl,
+ });
+ } else {
+ // mark as not accepted
+ }
+ terms?.retry()
+ }
+
+ const accepted = state.status === "accepted";
+
+ const base = {
+ error: undefined,
+ showingTermsOfService: {
+ value: showContent && (!accepted || showEvenIfaccepted),
+ button: {
+ onClick: accepted && !showEvenIfaccepted ? undefined : pushAlertOnError(async () => {
+ setShowContent(!showContent);
+ }),
+ },
+ },
+ terms: state,
+ termsAccepted: {
+ value: accepted,
+ button: {
+ onClick: readOnly ? undefined : pushAlertOnError(async () => {
+ const newValue = !accepted; //toggle
+ await onUpdate(newValue);
+ setShowContent(false);
+ }),
+ },
+ },
+ };
+
+ if (accepted) {
+ return {
+ status: "show-buttons-accepted",
+ ...base,
+ children,
+ };
+ }
+
+ if ((accepted && showEvenIfaccepted) || showContent) {
+ return {
+ status: "show-content",
+ error: undefined,
+ terms: state,
+ showingTermsOfService: readOnly ? undefined : base.showingTermsOfService,
+ termsAccepted: base.termsAccepted,
+ tosFormat: {
+ onChange: pushAlertOnError(async (s) => {
+ setFormat(s)
+ }),
+ list: supportedFormats,
+ value: format ?? ""
+ },
+ tosLang: {
+ onChange: pushAlertOnError(async (s) => {
+ setTosLang(s)
+ }),
+ list: supportedLangs,
+ value: tosLang ?? lang
+ }
+ };
+ }
+ //showing buttons
+ return {
+ status: "show-buttons-not-accepted",
+ ...base,
+ };
+
+}
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx
new file mode 100644
index 000000000..a28729eae
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/stories.tsx
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { ShowTosContentView } from "./views.js";
+import { ExchangeTosStatus } from "@gnu-taler/taler-util";
+
+export default {
+ title: "TermsOfService",
+};
+
+export const Ready = tests.createExample(ShowTosContentView, {
+ tosLang: {
+ list: {
+ es: "es",
+ en: "en",
+ },
+ value: "es",
+ onChange: (() => { }) as any
+ },
+ tosFormat: {
+ list: {
+ es: "es",
+ en: "en",
+ },
+ value: "es",
+ onChange: (() => { }) as any
+ },
+ terms: {
+ content: {
+ type: "plain",
+ content: "hola"
+ },
+ status: ExchangeTosStatus.Accepted,
+ version: "1"
+ },
+ status: "show-content",
+ termsAccepted: {
+ button: {},
+ }
+});
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/test.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/test.ts
new file mode 100644
index 000000000..170e7cad8
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/test.ts
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("Term of service states", () => {
+ it.skip("should assert", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts b/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts
new file mode 100644
index 000000000..96e268689
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/utils.ts
@@ -0,0 +1,108 @@
+/*
+ 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 {
+ ExchangeTosStatus,
+ GetExchangeTosResult,
+ Logger,
+} from "@gnu-taler/taler-util";
+
+export function buildTermsOfServiceState(
+ tos: GetExchangeTosResult,
+): TermsState {
+ const content: TermsDocument | undefined = parseTermsOfServiceContent(
+ tos.contentType,
+ tos.content,
+ );
+
+ return { content, status: tos.tosStatus, version: tos.currentEtag };
+}
+
+const logger = new Logger("termsofservice");
+
+function parseTermsOfServiceContent(
+ type: string,
+ text: string,
+): TermsDocument | undefined {
+ if (type === "text/xml") {
+ try {
+ const document = new DOMParser().parseFromString(text, "text/xml");
+ return { type: "xml", document };
+ } catch (e) {
+ logger.error("error parsing xml", e);
+ }
+ } else if (type === "text/html") {
+ try {
+ return { type: "html", html: text };
+ } catch (e) {
+ logger.error("error parsing url", e);
+ }
+ } else if (type === "text/json") {
+ try {
+ const data = JSON.parse(text);
+ return { type: "json", data };
+ } catch (e) {
+ logger.error("error parsing json", e);
+ }
+ } else if (type === "text/pdf") {
+ try {
+ const location = new URL(text);
+ return { type: "pdf", location };
+ } catch (e) {
+ logger.error("error parsing url", e);
+ }
+ }
+ const content = text;
+ return { type: "plain", content };
+}
+
+export type TermsState = {
+ content: TermsDocument | undefined;
+ status: ExchangeTosStatus;
+ version: string;
+};
+
+export type TermsDocument =
+ | TermsDocumentXml
+ | TermsDocumentHtml
+ | TermsDocumentPlain
+ | TermsDocumentJson
+ | TermsDocumentPdf;
+
+export interface TermsDocumentXml {
+ type: "xml";
+ document: Document;
+}
+
+export interface TermsDocumentHtml {
+ type: "html";
+ html: string;
+}
+
+export interface TermsDocumentPlain {
+ type: "plain";
+ content: string;
+}
+
+export interface TermsDocumentJson {
+ type: "json";
+ data: any;
+}
+
+export interface TermsDocumentPdf {
+ type: "pdf";
+ location: URL;
+}
diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
new file mode 100644
index 000000000..40cfba3bc
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx
@@ -0,0 +1,225 @@
+/*
+ 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 { ExchangeTosStatus } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { CheckboxOutlined } from "../../components/CheckboxOutlined.js";
+import { ExchangeXmlTos } from "../../components/ExchangeToS.js";
+import {
+ Input,
+ LinkSuccess,
+ TermsOfServiceStyle,
+ WarningBox
+} from "../../components/styled/index.js";
+import { Button } from "../../mui/Button.js";
+import { State } from "./index.js";
+import { SelectList } from "../SelectList.js";
+import { EnabledBySettings } from "../EnabledBySettings.js";
+
+export function ShowButtonsAcceptedTosView({
+ termsAccepted,
+ showingTermsOfService,
+ children,
+}: State.ShowButtonsAccepted): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ {showingTermsOfService.button.onClick !== undefined && (
+ <Fragment>
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <LinkSuccess
+ upperCased
+ onClick={showingTermsOfService.button.onClick}
+ >
+ <i18n.Translate>Show terms of service</i18n.Translate>
+ </LinkSuccess>
+ </section>
+ {termsAccepted.button.onClick !== undefined && (
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <CheckboxOutlined
+ name="terms"
+ enabled={termsAccepted.value}
+ label={
+ <i18n.Translate>
+ I accept the exchange terms of service
+ </i18n.Translate>
+ }
+ onToggle={termsAccepted.button.onClick}
+ />
+ </section>
+ )}
+ </Fragment>
+ )}
+ {children}
+ </Fragment>
+ );
+}
+
+export function ShowButtonsNonAcceptedTosView({
+ showingTermsOfService,
+ terms,
+}: State.ShowButtonsNotAccepted): VNode {
+ const { i18n } = useTranslationContext();
+ // const ableToReviewTermsOfService =
+ // showingTermsOfService.button.onClick !== undefined;
+
+ // if (!ableToReviewTermsOfService) {
+ // return (
+ // <Fragment>
+ // {terms.status === ExchangeTosStatus.Pending && (
+ // <section style={{ justifyContent: "space-around", display: "flex" }}>
+ // <WarningText>
+ // <i18n.Translate>
+ // Exchange doesn&apos;t have terms of service
+ // </i18n.Translate>
+ // </WarningText>
+ // </section>
+ // )}
+ // </Fragment>
+ // );
+ // }
+
+ return (
+ <Fragment>
+ {/* {terms.status === ExchangeTosStatus.NotFound && (
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <WarningText>
+ <i18n.Translate>
+ Exchange doesn&apos;t have terms of service
+ </i18n.Translate>
+ </WarningText>
+ </section>
+ )} */}
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={showingTermsOfService.button.onClick}
+ >
+ <i18n.Translate>Review exchange terms of service</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+ );
+}
+
+export function ShowTosContentView({
+ termsAccepted,
+ showingTermsOfService,
+ terms,
+ tosLang,
+ tosFormat,
+}: State.ShowContent): VNode {
+ const { i18n } = useTranslationContext();
+ const ableToReviewTermsOfService =
+ termsAccepted.button.onClick !== undefined;
+
+ return (
+ <section>
+ <Input style={{ display: "flex", justifyContent: "end" }}>
+ <EnabledBySettings name="selectTosFormat">
+ <SelectList
+ label={i18n.str`Format`}
+ list={tosFormat.list}
+ name="format"
+ value={tosFormat.value}
+ onChange={tosFormat.onChange}
+ />
+ </EnabledBySettings>
+ <SelectList
+ label={i18n.str`Language`}
+ list={tosLang.list}
+ name="lang"
+ value={tosLang.value}
+ onChange={tosLang.onChange}
+ />
+ </Input>
+
+ {!terms.content && (
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <WarningBox>
+ <i18n.Translate>
+ The exchange replied with a empty terms of service
+ </i18n.Translate>
+ </WarningBox>
+ </section>
+ )}
+ {terms.content && (
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ {terms.content.type === "xml" &&
+ (!terms.content.document ? (
+ <WarningBox>
+ <i18n.Translate>
+ No terms of service. The exchange replied with a empty
+ document
+ </i18n.Translate>
+ </WarningBox>
+ ) : (
+ <TermsOfServiceStyle>
+ <ExchangeXmlTos doc={terms.content.document} />
+ </TermsOfServiceStyle>
+ ))}
+ {terms.content.type === "plain" &&
+ (!terms.content.content ? (
+ <WarningBox>
+ <i18n.Translate>
+ No terms of service. The exchange replied with a empty text
+ </i18n.Translate>
+ </WarningBox>
+ ) : (
+ <div style={{ textAlign: "left" }}>
+ <pre>{terms.content.content}</pre>
+ </div>
+ ))}
+ {terms.content.type === "html" && (
+ <iframe style={{ width: "100%" }} srcDoc={terms.content.html} />
+ )}
+ {terms.content.type === "pdf" && (
+ <a href={terms.content.location.toString()} download="tos.pdf">
+ <i18n.Translate>Download Terms of Service</i18n.Translate>
+ </a>
+ )}
+ </section>
+ )}
+ {showingTermsOfService && ableToReviewTermsOfService && (
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <LinkSuccess
+ upperCased
+ onClick={showingTermsOfService.button.onClick}
+ >
+ <i18n.Translate>Hide terms of service</i18n.Translate>
+ </LinkSuccess>
+ </section>
+ )}
+ {termsAccepted.button.onClick && terms.status !== ExchangeTosStatus.Accepted && (
+ <section style={{ justifyContent: "space-around", display: "flex" }}>
+ <CheckboxOutlined
+ name="terms"
+ enabled={termsAccepted.value}
+ label={
+ <i18n.Translate>
+ I accept the exchange terms of service
+ </i18n.Translate>
+ }
+ onToggle={termsAccepted.button.onClick}
+ />
+ </section>
+ )}
+ </section>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/Time.tsx b/packages/taler-wallet-webextension/src/components/Time.tsx
new file mode 100644
index 000000000..eee295756
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Time.tsx
@@ -0,0 +1,46 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import { formatISO, format } from "date-fns";
+import { h, VNode } from "preact";
+
+/**
+ *
+ * @deprecated use web-util
+ * @returns
+ */
+export function Time({
+ timestamp,
+ format: formatString,
+}: {
+ timestamp: AbsoluteTime | undefined;
+ format: string;
+}): VNode {
+ return (
+ <time
+ dateTime={
+ !timestamp || timestamp.t_ms === "never"
+ ? undefined
+ : formatISO(timestamp.t_ms)
+ }
+ >
+ {!timestamp || timestamp.t_ms === "never"
+ ? "never"
+ : format(timestamp.t_ms, formatString)}
+ </time>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
deleted file mode 100644
index 991e97c94..000000000
--- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { AmountString, Timestamp, Transaction, TransactionType } from '@gnu-taler/taler-util';
-import { format, formatDistance } from 'date-fns';
-import { h } from 'preact';
-import imageBank from '../../static/img/ri-bank-line.svg';
-import imageHandHeart from '../../static/img/ri-hand-heart-line.svg';
-import imageRefresh from '../../static/img/ri-refresh-line.svg';
-import imageRefund from '../../static/img/ri-refund-2-line.svg';
-import imageShoppingCart from '../../static/img/ri-shopping-cart-line.svg';
-import { Pages } from "../NavigationBar";
-import { Column, ExtraLargeText, HistoryRow, SmallLightText, LargeText, LightText } from './styled/index';
-
-export function TransactionItem(props: { tx: Transaction, multiCurrency: boolean }): JSX.Element {
- const tx = props.tx;
- switch (tx.type) {
- case TransactionType.Withdrawal:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- title={new URL(tx.exchangeBaseUrl).hostname}
- timestamp={tx.timestamp}
- iconPath={imageBank}
- pending={tx.pending}
- multiCurrency={props.multiCurrency}
- />
- );
- case TransactionType.Payment:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"debit"}
- title={tx.info.merchant.name}
- subtitle={tx.info.summary}
- timestamp={tx.timestamp}
- iconPath={imageShoppingCart}
- pending={tx.pending}
- multiCurrency={props.multiCurrency}
- />
- );
- case TransactionType.Refund:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- title={tx.info.merchant.name}
- timestamp={tx.timestamp}
- iconPath={imageRefund}
- pending={tx.pending}
- multiCurrency={props.multiCurrency}
- />
- );
- case TransactionType.Tip:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- title={new URL(tx.merchantBaseUrl).hostname}
- timestamp={tx.timestamp}
- iconPath={imageHandHeart}
- pending={tx.pending}
- multiCurrency={props.multiCurrency}
- />
- );
- case TransactionType.Refresh:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"credit"}
- title={new URL(tx.exchangeBaseUrl).hostname}
- timestamp={tx.timestamp}
- iconPath={imageRefresh}
- pending={tx.pending}
- multiCurrency={props.multiCurrency}
- />
- );
- case TransactionType.Deposit:
- return (
- <TransactionLayout
- id={tx.transactionId}
- amount={tx.amountEffective}
- debitCreditIndicator={"debit"}
- title={tx.targetPaytoUri}
- timestamp={tx.timestamp}
- iconPath={imageRefresh}
- pending={tx.pending}
- multiCurrency={props.multiCurrency}
- />
- );
- }
-}
-
-function TransactionLayout(props: TransactionLayoutProps): JSX.Element {
- const date = new Date(props.timestamp.t_ms);
- const dateStr = format(date, 'dd MMM, hh:mm')
-
- return (
- <HistoryRow href={Pages.transaction.replace(':tid', props.id)}>
- <img src={props.iconPath} />
- <Column>
- <LargeText>
- <div>{props.title}</div>
- {props.subtitle && <div style={{color:'gray', fontSize:'medium', marginTop: 5}}>{props.subtitle}</div>}
- </LargeText>
- {props.pending &&
- <LightText style={{ marginTop: 5, marginBottom: 5 }}>Waiting for confirmation</LightText>
- }
- <SmallLightText style={{marginTop:5 }}>{dateStr}</SmallLightText>
- </Column>
- <TransactionAmount
- pending={props.pending}
- amount={props.amount}
- multiCurrency={props.multiCurrency}
- debitCreditIndicator={props.debitCreditIndicator}
- />
- </HistoryRow>
- );
-}
-
-interface TransactionLayoutProps {
- debitCreditIndicator: "debit" | "credit" | "unknown";
- amount: AmountString | "unknown";
- timestamp: Timestamp;
- title: string;
- subtitle?: string;
- id: string;
- iconPath: string;
- pending: boolean;
- multiCurrency: boolean;
-}
-
-interface TransactionAmountProps {
- debitCreditIndicator: "debit" | "credit" | "unknown";
- amount: AmountString | "unknown";
- pending: boolean;
- multiCurrency: boolean;
-}
-
-function TransactionAmount(props: TransactionAmountProps): JSX.Element {
- const [currency, amount] = props.amount.split(":");
- let sign: string;
- switch (props.debitCreditIndicator) {
- case "credit":
- sign = "+";
- break;
- case "debit":
- sign = "-";
- break;
- case "unknown":
- sign = "";
- }
- return (
- <Column style={{
- textAlign: 'center',
- color:
- props.pending ? "gray" :
- (sign === '+' ? 'darkgreen' :
- (sign === '-' ? 'darkred' :
- undefined))
- }}>
- <ExtraLargeText>
- {sign}
- {amount}
- </ExtraLargeText>
- {props.multiCurrency && <div>{currency}</div>}
- {props.pending && <div>PENDING</div>}
- </Column>
- );
-}
-
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
new file mode 100644
index 000000000..a77a69fa6
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
@@ -0,0 +1,1050 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AbsoluteTime,
+ NotificationType,
+ ObservabilityEventType,
+ RequestProgressNotification,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TaskProgressNotification,
+ WalletNotification,
+ assertUnreachable
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, 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 { TextField } from "../mui/TextField.js";
+import { SafeHandler } from "../mui/handlers.js";
+import { WxApiType } from "../wxApi.js";
+import { WalletActivityTrack } from "../wxBackend.js";
+import { Modal } from "./Modal.js";
+import { Time } from "./Time.js";
+
+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);
+
+ useEffect(() => {
+ document.body.style.marginBottom = `${
+ collapsed ? CLOSE_ACTIVITY_HEIGHT_PX : OPEN_ACTIVITY_HEIGHT_PX
+ }px`;
+ return () => {
+ 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>
+ );
+ }
+ 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("events");
+ }}
+ >
+ <i18n.Translate>Events</i18n.Translate>
+ </Button>
+ <Button
+ variant={table === "tasks" ? "contained" : "outlined"}
+ style={{ margin: 4 }}
+ onClick={async () => {
+ setTable("tasks");
+ }}
+ >
+ <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>
+ <div
+ style={{
+ backgroundColor: "white",
+ }}
+ >
+ {(function (): VNode {
+ switch (table) {
+ case "events": {
+ return <ObservabilityEventsTable />;
+ }
+ case "tasks": {
+ return <ActiveTasksTable />;
+ }
+ default: {
+ assertUnreachable(table);
+ }
+ }
+ })()}
+ </div>
+ </div>
+ );
+}
+
+interface MoreInfoPRops {
+ events: (WalletNotification & { when: AbsoluteTime })[];
+ 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>
+ );
+}
+
+function ShowBackupOperationError({ events, onClick }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.BackupOperationError) return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Error</dt>
+ <dd>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ const error = not.error;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time timestamp={error.when} format="yyyy/MM/dd HH:mm:ss" />
+ </dd>
+ </dl>
+ <pre
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {TalerErrorCode[not.error.code]}
+ </a>
+ </dd>
+ </Fragment>
+ );
+}
+
+function ShowTransactionStateTransition({
+ events,
+ onClick,
+}: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.TransactionStateTransition)
+ return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Old state</dt>
+ <dd>
+ {not.oldTxState.major} - {not.oldTxState.minor ?? ""}
+ </dd>
+ <dt>New state</dt>
+ <dd>
+ {not.newTxState.major} - {not.newTxState.minor ?? ""}
+ </dd>
+ <dt>Transaction</dt>
+ <dd>
+ <a
+ title={not.transactionId}
+ href={Pages.balanceTransaction({ tid: not.transactionId })}
+ >
+ {not.transactionId.substring(0, 10)}
+ </a>
+ </dd>
+ {not.errorInfo ? (
+ <Fragment>
+ <dt>Error</dt>
+ <dd>
+ <a
+ href="#"
+ onClick={(e) => {
+ if (!not.errorInfo) return;
+ e.preventDefault();
+ const error = not.errorInfo;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Message</dt>
+ <dd>{error.message ?? "--"}</dd>
+ </dl>
+ </Fragment>,
+ );
+ }}
+ >
+ {TalerErrorCode[not.errorInfo.code]}
+ </a>
+ </dd>
+ </Fragment>
+ ) : undefined}
+ <dt>Experimental</dt>
+ <dd>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(not.experimentalUserData, undefined, 2)}
+ </pre>
+ </dd>
+ </Fragment>
+ );
+}
+function ShowExchangeStateTransition({ events }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.ExchangeStateTransition)
+ return <Fragment />;
+ return (
+ <Fragment>
+ <dt>Exchange</dt>
+ <dd>{not.exchangeBaseUrl}</dd>
+ {not.oldExchangeState &&
+ not.newExchangeState.exchangeEntryStatus !==
+ not.oldExchangeState?.exchangeEntryStatus && (
+ <Fragment>
+ <dt>Entry status</dt>
+ <dd>
+ from {not.oldExchangeState.exchangeEntryStatus} to{" "}
+ {not.newExchangeState.exchangeEntryStatus}
+ </dd>
+ </Fragment>
+ )}
+ {not.oldExchangeState &&
+ not.newExchangeState.exchangeUpdateStatus !==
+ not.oldExchangeState?.exchangeUpdateStatus && (
+ <Fragment>
+ <dt>Update status</dt>
+ <dd>
+ from {not.oldExchangeState.exchangeUpdateStatus} to{" "}
+ {not.newExchangeState.exchangeUpdateStatus}
+ </dd>
+ </Fragment>
+ )}
+ {not.oldExchangeState &&
+ not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && (
+ <Fragment>
+ <dt>Tos status</dt>
+ <dd>
+ from {not.oldExchangeState.tosStatus} to{" "}
+ {not.newExchangeState.tosStatus}
+ </dd>
+ </Fragment>
+ )}
+ </Fragment>
+ );
+}
+
+type ObservaNotifWithTime = (
+ | TaskProgressNotification
+ | RequestProgressNotification
+) & {
+ when: AbsoluteTime;
+};
+function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode {
+ // let prev: ObservaNotifWithTime;
+ const asd = events.map((not, 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.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError:
+ case ObservabilityEventType.DbQueryStart:
+ return "Database";
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError:
+ case ObservabilityEventType.RequestStart:
+ return "Wallet";
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError:
+ case ObservabilityEventType.CryptoStart:
+ return "Crypto";
+ case ObservabilityEventType.TaskStart:
+ return "Task start";
+ case ObservabilityEventType.TaskStop:
+ return "Task stop";
+ case ObservabilityEventType.TaskReset:
+ return "Task reset";
+ case ObservabilityEventType.ShepherdTaskResult:
+ return "Schedule";
+ case ObservabilityEventType.DeclareTaskDependency:
+ return "Task dependency";
+ case ObservabilityEventType.Message:
+ return "Message";
+ }
+ })();
+
+ return (
+ <ShowObervavilityDetails
+ 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 {
+ switch (notif.event.type) {
+ case ObservabilityEventType.HttpFetchStart:
+ case ObservabilityEventType.HttpFetchFinishError:
+ case ObservabilityEventType.HttpFetchFinishSuccess: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>
+ {notif.event.url}{" "}
+ {prev?.event.type ===
+ ObservabilityEventType.HttpFetchFinishSuccess ? (
+ `(${prev.event.status})`
+ ) : prev?.event.type ===
+ ObservabilityEventType.HttpFetchFinishError ? (
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ if (
+ prev.event.type !==
+ ObservabilityEventType.HttpFetchFinishError
+ )
+ return;
+ const error = prev.event.error;
+ onClick(
+ <Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>
+ {TalerErrorCode[error.code]} ({error.code})
+ </dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd>
+ <Time
+ timestamp={error.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </dd>
+ </dl>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ fail
+ </a>
+ ) : undefined}
+ </td>
+ <td>
+ {" "}
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ {" "}
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.DbQueryStart:
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>
+ {notif.event.location} {notif.event.name}
+ </td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+
+ case ObservabilityEventType.TaskStart:
+ case ObservabilityEventType.TaskStop:
+ case ObservabilityEventType.DeclareTaskDependency:
+ case ObservabilityEventType.TaskReset: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.taskId}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.ShepherdTaskResult: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.resultType}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.CryptoStart:
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.operation}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.RequestStart:
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError: {
+ return (
+ <tr>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ onClick(
+ <Fragment>
+ <pre
+ style={{
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>,
+ );
+ }}
+ >
+ {title}
+ </a>
+ </td>
+ <td>{notif.event.type}</td>
+ <td>
+ <Time timestamp={notif.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ <td>
+ <Time timestamp={prev?.when} format="yyyy/MM/dd HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ }
+ case ObservabilityEventType.Message:
+ // FIXME
+ return <></>;
+ }
+}
+
+function refresh(
+ api: WxApiType,
+ onUpdate: (list: WalletActivityTrack[]) => void,
+ filter: string,
+) {
+ api.background
+ .call("getNotifications", { filter })
+ .then((notif) => {
+ onUpdate(notif);
+ })
+ .catch((error) => {
+ console.log(error);
+ });
+}
+
+export function ObservabilityEventsTable(): VNode {
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+
+ 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, filter);
+
+ lastTimeout = setTimeout(() => {
+ periodicRefresh();
+ }, 1000);
+
+ return () => {
+ clearTimeout(lastTimeout);
+ };
+ }
+ return periodicRefresh();
+ }, [filter]);
+
+ 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>
+ {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>
+ </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 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();
+ const api = useBackendContext();
+ const state = useAsyncAsHook(() => {
+ return api.wallet.call(WalletApiOperation.GetActiveTasks, {});
+ });
+ const [showError, setShowError] = useState<TalerErrorDetail>();
+ const tasks = state && !state.hasError ? state.response.tasks : [];
+
+ useEffect(() => {
+ if (!state || state.hasError) return;
+ const lastTimeout = setTimeout(() => {
+ state.retry();
+ }, 1000);
+ return () => {
+ clearTimeout(lastTimeout);
+ };
+ }, [tasks]);
+
+ return (
+ <Fragment>
+ {showError && (
+ <ErroDetailModal
+ error={showError}
+ onClose={async () => {
+ setShowError(undefined);
+ }}
+ />
+ )}
+
+ <table style={{ width: "100%" }}>
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Type</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Id</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Since</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Next try</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Error</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Transaction</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {tasks.map((task) => {
+ const [type, id] = task.taskId.split(":");
+ return (
+ <tr 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/components/index.stories.tsx b/packages/taler-wallet-webextension/src/components/index.stories.tsx
new file mode 100644
index 000000000..4a7a068d3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/index.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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)
+ */
+
+export * as a1 from "./Banner.stories.js";
+export * as a2 from "./PendingTransactions.stories.js";
+export * as a3 from "./Amount.stories.js";
+export * as a4 from "./ShowFullContractTermPopup.stories.js";
+export * as a5 from "./TermsOfService/stories.js";
+export * as a6 from "./QR.stories.js";
+export * as a7 from "./AmountField.stories.js";
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 65c1f49e9..739b71064 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,18 +14,18 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-
// need to import linaria types, otherwise compiler will complain
-import type * as Linaria from '@linaria/core';
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+// import type * as Linaria from "@linaria/core";
-import { styled } from '@linaria/react';
+import { styled } from "@linaria/react";
export const PaymentStatus = styled.div<{ color: string }>`
padding: 5px;
border-radius: 5px;
color: white;
- background-color: ${p => p.color};
-`
+ background-color: ${(p: any) => p.color};
+`;
export const WalletAction = styled.div`
display: flex;
@@ -35,19 +35,28 @@ export const WalletAction = styled.div`
align-items: center;
margin: auto;
- height: 100%;
-
+
& h1:first-child {
- margin-top: 0;
+ margin-top: 0;
+ }
+ & > * {
+ width: 600px;
}
section {
margin-bottom: 2em;
- & button {
+ table td {
+ padding: 5px 5px;
+ }
+ table tr {
+ border-bottom: 1px solid black;
+ border-top: 1px solid black;
+ }
+ button {
margin-right: 8px;
margin-left: 8px;
}
}
-`
+`;
export const WalletActionOld = styled.section`
border: solid 5px black;
border-radius: 10px;
@@ -59,39 +68,53 @@ export const WalletActionOld = styled.section`
margin: auto;
height: 100%;
-
+
& h1:first-child {
- margin-top: 0;
+ margin-top: 0;
}
-`
+`;
+
+export const Title = styled.h1`
+ font-size: 2em;
+ margin-top: 1em;
+ margin-bottom: 1em;
+`;
+export const SubTitle = styled.h1`
+ font-size: 1.5em;
+ margin-top: 1em;
+ margin-bottom: 1em;
+`;
export const DateSeparator = styled.div`
color: gray;
- margin: .2em;
+ margin: 0.2em;
margin-top: 1em;
-`
+`;
export const WalletBox = styled.div<{ noPadding?: boolean }>`
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
& > * {
- width: 400px;
+ width: 800px;
}
& > section {
- padding-left: ${({ noPadding }) => noPadding ? '0px' : '8px'};
- padding-right: ${({ noPadding }) => noPadding ? '0px' : '8px'};
- // this margin will send the section up when used with a header
- margin-bottom: auto;
+ padding: ${({ noPadding }: any) => (noPadding ? "0px" : "8px")};
+
+ margin-bottom: auto;
overflow: auto;
table td {
- padding: 5px 10px;
+ padding: 5px 5px;
}
table tr {
border-bottom: 1px solid black;
border-top: 1px solid black;
}
+ button {
+ margin-right: 8px;
+ margin-left: 8px;
+ }
}
& > header {
@@ -122,31 +145,31 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
flex-direction: row;
justify-content: space-between;
display: flex;
- background-color: #f7f7f7;
- & button {
+ button {
margin-right: 8px;
margin-left: 8px;
}
}
-`
+`;
export const Middle = styled.div`
- justify-content: space-around;
- display: flex;
- flex-direction: column;
- height: 100%;
-`
+ justify-content: space-around;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+`;
export const PopupBox = styled.div<{ noPadding?: boolean }>`
height: 290px;
- width: 400px;
+ width: 500px;
+ overflow-y: visible;
display: flex;
flex-direction: column;
justify-content: space-between;
& > section {
- padding: ${({ noPadding }) => noPadding ? '0px' : '8px'};
+ padding: ${({ noPadding }: any) => (noPadding ? "0px" : "8px")};
// this margin will send the section up when used with a header
- margin-bottom: auto;
+ margin-bottom: auto;
overflow-y: auto;
table td {
@@ -156,6 +179,10 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`
border-bottom: 1px solid black;
border-top: 1px solid black;
}
+ button {
+ margin-right: 8px;
+ margin-left: 8px;
+ }
}
& > section[data-expanded] {
@@ -196,36 +223,207 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`
flex-direction: row;
justify-content: space-between;
display: flex;
- & button {
+ button {
margin-right: 8px;
margin-left: 8px;
}
}
+`;
+
+export const TableWithRoundRows = styled.table`
+ border-collapse: separate;
+ border-spacing: 0px 10px;
+ margin-top: -10px;
+
+ td {
+ border: solid 1px #000;
+ border-style: solid none;
+ padding: 10px;
+ }
+ td:first-child {
+ border-left-style: solid;
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+ }
+ td:last-child {
+ border-right-style: solid;
+ border-bottom-right-radius: 5px;
+ border-top-right-radius: 5px;
+ }
+`;
+
+const Tooltip = styled.div<{ content: string }>`
+ display: block;
+ position: relative;
+
+ ::before {
+ position: absolute;
+ z-index: 1000001;
+ width: 0;
+ height: 0;
+ color: darkgray;
+ pointer-events: none;
+ content: "";
+ border: 6px solid transparent;
+
+ border-bottom-color: darkgray;
+ }
+
+ ::after {
+ position: absolute;
+ z-index: 1000001;
+ padding: 0.5em 0.75em;
+ font: normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI",
+ Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+ -webkit-font-smoothing: subpixel-antialiased;
+ color: white;
+ text-align: center;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ letter-spacing: normal;
+ word-wrap: break-word;
+ white-space: pre;
+ pointer-events: none;
+ content: attr(content);
+ background: darkgray;
+ border-radius: 6px;
+ }
+`;
+
+export const TooltipBottom = styled(Tooltip)`
+ ::before {
+ top: auto;
+ right: 50%;
+ bottom: -7px;
+ margin-right: -6px;
+ }
+ ::after {
+ top: 100%;
+ right: -50%;
+ margin-top: 6px;
+ }
+`;
+
+export const TooltipRight = styled(Tooltip)`
+ ::before {
+ top: 0px;
+ left: 16px;
+ transform: rotate(-90deg);
+ }
+ ::after {
+ top: -50%;
+ left: 28px;
+ margin-top: 6px;
+ }
+`;
+
+export const TooltipLeft = styled(Tooltip)`
+ ::before {
+ top: 0px;
+ right: 16px;
+ transform: rotate(90deg);
+ }
+ ::after {
+ top: -50%;
+ right: 28px;
+ margin-top: 6px;
+ }
+`;
+
+export const Overlay = styled.div`
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 2;
+ cursor: pointer;
+`;
+
+export const NotifyUpdateFadeOut = styled.div`
+ border: 2px solid red;
+ transition: all 0.4s ease-out;
+ animation: fadeout 1s forwards;
+ animation-delay: 0.1s;
+ @keyframes fadeout {
+ to {
+ border-color: #f5f5f5;
+ }
+ }
+`;
+
+export const CenteredDialog = styled.div`
+ position: absolute;
+ text-align: left;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ top: 50%;
+ left: 50%;
+ /* font-size: 50px; */
+ color: black;
+ transform: translate(-50%, -50%);
+ -ms-transform: translate(-50%, -50%);
+ cursor: initial;
+ background-color: white;
+ border-radius: 10px;
-`
+ max-height: 70%;
+
+ & > header {
+ border-top-right-radius: 6px;
+ border-top-left-radius: 6px;
+ padding: 10px;
+ background-color: #f5f5f5;
+ border-bottom: 1px solid #dbdbdb;
+ font-weight: bold;
+ }
+ & > section {
+ padding: 10px;
+ flex-grow: 1;
+ flex-shrink: 1;
+ overflow: auto;
+ }
+ & > footer {
+ border-top: 1px solid #dbdbdb;
+ border-bottom-right-radius: 6px;
+ border-bottom-left-radius: 6px;
+ padding: 10px;
+ display: flex;
+ justify-content: space-between;
+ }
+`;
export const Button = styled.button<{ upperCased?: boolean }>`
display: inline-block;
- zoom: 1;
+ /* zoom: 1; */
line-height: normal;
white-space: nowrap;
- vertical-align: middle;
- text-align: center;
+ vertical-align: middle; //check this
+ /* text-align: center; */
cursor: pointer;
user-select: none;
box-sizing: border-box;
- text-transform: ${({ upperCased }) => upperCased ? 'uppercase' : 'none'};
+ text-transform: ${({ upperCased }: any) =>
+ upperCased ? "uppercase" : "none"};
font-family: inherit;
font-size: 100%;
padding: 0.5em 1em;
- color: #444; /* rgba not supported (IE 8) */
+ /* color: #444; rgba not supported (IE 8) */
color: rgba(0, 0, 0, 0.8); /* rgba supported */
border: 1px solid #999; /*IE 6/7/8*/
- border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/
- background-color: '#e6e6e6';
+ /* border: none rgba(0, 0, 0, 0); IE9 + everything else */
+ background-color: "#e6e6e6";
text-decoration: none;
border-radius: 2px;
+ text-transform: uppercase;
:focus {
outline: 0;
@@ -244,11 +442,11 @@ export const Button = styled.button<{ upperCased?: boolean }>`
}
:hover {
- filter: alpha(opacity=90);
+ filter: alpha(opacity=80);
background-image: linear-gradient(
transparent,
- rgba(0, 0, 0, 0.05) 40%,
- rgba(0, 0, 0, 0.1)
+ rgba(0, 0, 0, 0.1) 40%,
+ rgba(0, 0, 0, 0.2)
);
}
`;
@@ -258,12 +456,13 @@ export const Link = styled.a<{ upperCased?: boolean }>`
zoom: 1;
line-height: normal;
white-space: nowrap;
- vertical-align: middle;
+ /* vertical-align: middle; */
text-align: center;
cursor: pointer;
user-select: none;
box-sizing: border-box;
- text-transform: ${({ upperCased }) => upperCased ? 'uppercase' : 'none'};
+ text-transform: ${({ upperCased }: any) =>
+ upperCased ? "uppercase" : "none"};
font-family: inherit;
font-size: 100%;
@@ -304,11 +503,10 @@ export const FontIcon = styled.div`
text-align: center;
font-weight: bold;
/* vertical-align: text-top; */
-`
+`;
export const ButtonBox = styled(Button)`
- padding: .5em;
- width: fit-content;
- height: 2em;
+ padding: 8px;
+ /* font-size: small; */
& > ${FontIcon} {
width: 1em;
@@ -316,95 +514,107 @@ export const ButtonBox = styled(Button)`
display: inline;
line-height: 0px;
}
- background-color: transparent;
+ background-color: #f7f7f7;
border: 1px solid;
border-radius: 4px;
border-color: black;
color: black;
-`
-
+ /* text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); */
+ /* -webkit-border-horizontal-spacing: 0px;
+ -webkit-border-vertical-spacing: 0px; */
+`;
const ButtonVariant = styled(Button)`
color: white;
border-radius: 4px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
-`
+`;
+
+export const LinkDestructive = styled(Link)`
+ background-color: rgb(202, 60, 60);
+`;
-export const ButtonPrimary = styled(ButtonVariant)`
- background-color: rgb(66, 184, 221);
-`
+export const LinkPrimary = styled(Link)`
+ color: black;
+`;
+
+export const ButtonPrimary = styled(ButtonVariant) <{ small?: boolean }>`
+ font-size: ${({ small }: any) => (small ? "small" : "inherit")};
+ background-color: #0042b2;
+ border-color: #0042b2;
+`;
export const ButtonBoxPrimary = styled(ButtonBox)`
- color: rgb(66, 184, 221);
- border-color: rgb(66, 184, 221);
-`
+ color: #0042b2;
+ border-color: #0042b2;
+`;
export const ButtonSuccess = styled(ButtonVariant)`
background-color: #388e3c;
-`
+`;
export const LinkSuccess = styled(Link)`
color: #388e3c;
-`
+`;
export const ButtonBoxSuccess = styled(ButtonBox)`
color: #388e3c;
border-color: #388e3c;
-`
+`;
export const ButtonWarning = styled(ButtonVariant)`
background-color: rgb(223, 117, 20);
-`
+`;
export const LinkWarning = styled(Link)`
color: rgb(223, 117, 20);
-`
+`;
export const ButtonBoxWarning = styled(ButtonBox)`
color: rgb(223, 117, 20);
border-color: rgb(223, 117, 20);
-`
+`;
export const ButtonDestructive = styled(ButtonVariant)`
background-color: rgb(202, 60, 60);
-`
+`;
export const ButtonBoxDestructive = styled(ButtonBox)`
color: rgb(202, 60, 60);
border-color: rgb(202, 60, 60);
-`
-
+`;
export const BoldLight = styled.div`
-color: gray;
-font-weight: bold;
-`
+ color: gray;
+ font-weight: bold;
+`;
export const Centered = styled.div`
text-align: center;
& > :not(:first-child) {
margin-top: 15px;
}
-`
+`;
+
export const Row = styled.div`
display: flex;
margin: 0.5em 0;
justify-content: space-between;
padding: 0.5em;
-`
+`;
export const Row2 = styled.div`
display: flex;
/* margin: 0.5em 0; */
justify-content: space-between;
padding: 0.5em;
-`
+`;
export const Column = styled.div`
display: flex;
flex-direction: column;
margin: 0em 1em;
justify-content: space-between;
-`
+`;
export const RowBorderGray = styled(Row)`
border: 1px solid gray;
/* border-radius: 0.5em; */
-`
+`;
export const RowLightBorderGray = styled(Row2)`
border: 1px solid lightgray;
@@ -414,7 +624,7 @@ export const RowLightBorderGray = styled(Row2)`
border: 1px solid lightgray;
background-color: red;
}
-`
+`;
export const HistoryRow = styled.a`
text-decoration: none;
@@ -423,7 +633,7 @@ export const HistoryRow = styled.a`
display: flex;
justify-content: space-between;
padding: 0.5em;
-
+
border: 1px solid lightgray;
border-top: 0px;
@@ -439,7 +649,7 @@ export const HistoryRow = styled.a`
margin-left: auto;
align-self: center;
}
-`
+`;
export const ListOfProducts = styled.div`
& > div > a > img {
@@ -453,83 +663,123 @@ export const ListOfProducts = styled.div`
margin-right: auto;
margin-left: 1em;
}
-`
+`;
export const LightText = styled.div`
color: gray;
-`
+`;
+
+export const SuccessText = styled.div`
+ color: #388e3c;
+`;
+
+export const DestructiveText = styled.div`
+ color: rgb(202, 60, 60);
+`;
export const WarningText = styled.div`
color: rgb(223, 117, 20);
-`
+`;
export const SmallText = styled.div`
- font-size: small;
-`
+ font-size: small;
+`;
+
+export const SmallBoldText = styled.div`
+ font-size: small;
+ font-weight: bold;
+`;
+
+export const AgeSign = styled.div<{size:number}>`
+ display: inline-block;
+ border: red solid 1px;
+ border-radius: 100%;
+ width: ${({ size }: {size:number}) => (`${size}px`)};
+ height: ${({ size }: {size:number}) => (`${size}px`)};
+ line-height: ${({ size }: {size:number}) => (`${size}px`)};
+ padding: 3px;
+`;
+
export const LargeText = styled.div`
- font-size: large;
-`
+ font-size: large;
+`;
export const ExtraLargeText = styled.div`
- font-size: x-large;
-`
+ font-size: x-large;
+`;
export const SmallLightText = styled(SmallText)`
color: gray;
-`
+`;
export const CenteredText = styled.div`
white-space: nowrap;
text-align: center;
-`
+`;
export const CenteredBoldText = styled(CenteredText)`
white-space: nowrap;
text-align: center;
font-weight: bold;
color: ${((props: any): any => String(props.color) as any) as any};
-`
+`;
export const Input = styled.div<{ invalid?: boolean }>`
& label {
display: block;
padding: 5px;
- color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
+ color: ${({ invalid }: any) => (!invalid ? "inherit" : "red")};
}
& input {
display: block;
padding: 5px;
width: calc(100% - 4px - 10px);
- border-color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
+ border-color: ${({ invalid }: any) => (!invalid ? "inherit" : "red")};
}
-`
+`;
export const InputWithLabel = styled.div<{ invalid?: boolean }>`
+ /* display: flex; */
+
& label {
display: block;
+ font-weight: bold;
+ margin-left: 0.5em;
padding: 5px;
- color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
+ color: ${({ invalid }: any) => (!invalid ? "inherit" : "red")};
}
- & > div {
- position: relative;
- display: flex;
- top: 0px;
- bottom: 0px;
-
- & > div {
- position: absolute;
- background-color: lightgray;
- padding: 5px;
- margin: 2px;
- }
- & > input {
- flex: 1;
- padding: 5px;
- border-color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
- }
+ & div {
+ line-height: 24px;
+ display: flex;
+ }
+ & div > span {
+ background-color: lightgray;
+ box-sizing: border-box;
+ border-bottom-left-radius: 0.25em;
+ border-top-left-radius: 0.25em;
+ height: 2em;
+ display: inline-block;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+ align-items: center;
+ display: flex;
+ }
+ & input {
+ border-width: 1px;
+ box-sizing: border-box;
+ height: 2em;
+ /* border-color: lightgray; */
+ border-bottom-right-radius: 0.25em;
+ border-top-right-radius: 0.25em;
+ border-color: ${({ invalid }: any) => (!invalid ? "lightgray" : "red")};
}
-`
+ margin-bottom: 16px;
+`;
+
+export const ErrorText = styled.div`
+ color: red;
+`;
export const ErrorBox = styled.div`
border: 2px solid #f5c6cb;
@@ -539,6 +789,7 @@ export const ErrorBox = styled.div`
flex-direction: column;
/* margin: 0.5em; */
padding: 1em;
+ margin: 1em;
/* width: 100%; */
color: #721c24;
background: #f8d7da;
@@ -555,49 +806,105 @@ export const ErrorBox = styled.div`
width: 28px;
}
}
-`
+`;
+
+export const RedBanner = styled.div`
+ width: 80%;
+ padding: 4px;
+ text-align: center;
+ background: red;
+ border: 1px solid #df3a3a;
+ border-radius: 4px;
+ font-weight: bold;
+ margin: 4px;
+`;
+
+export const InfoBox = styled(ErrorBox)`
+ color: black;
+ background-color: #d1e7dd;
+ border-color: #badbcc;
+`;
export const SuccessBox = styled(ErrorBox)`
color: #0f5132;
background-color: #d1e7dd;
border-color: #badbcc;
-`
+`;
export const WarningBox = styled(ErrorBox)`
color: #664d03;
background-color: #fff3cd;
border-color: #ffecb5;
-`
+`;
+
+export const NavigationHeaderHolder = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background-color: #0042b2;
+`;
-export const PopupNavigation = styled.div<{ devMode?: boolean }>`
- background-color:#0042b2;
+export const NavigationHeader = styled.div`
+ background-color: #0042b2;
height: 35px;
justify-content: space-around;
display: flex;
- & > div {
- width: 400px;
+ & {
+ width: 500px;
}
- & > div > a {
+ & > a,
+ & > div {
color: #f8faf7;
display: inline-block;
- width: calc(400px / ${({ devMode }) => !devMode ? 4 : 5});
+ width: 100%;
text-align: center;
text-decoration: none;
vertical-align: middle;
line-height: 35px;
}
- & > div > a.active {
+ & > a.active {
background-color: #f8faf7;
color: #0042b2;
font-weight: bold;
}
`;
-export const NiceSelect = styled.div`
+interface SvgIconProps {
+ title: string;
+ color: string;
+ onClick?: any;
+ transform?: string;
+}
+export const SvgIcon = styled.div<SvgIconProps>`
+ & > svg {
+ fill: ${({ color }: any) => color};
+ transform: ${({ transform }: any) => (transform ? transform : "")};
+ }
+ /* width: 24px;
+ height: 24px; */
+ margin-left: 8px;
+ margin-right: 8px;
+ display: inline;
+ padding: 4px;
+ cursor: ${({ onClick }: any) => (onClick ? "pointer" : "inherit")};
+`;
+
+export const Icon = styled.div`
+ background-color: gray;
+ width: 24px;
+ height: 24px;
+ margin-left: 8px;
+ margin-right: 8px;
+ padding: 4px;
+`;
+
+const image = `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`;
+export const NiceSelect = styled.div`
& > select {
-webkit-appearance: none;
-moz-appearance: none;
@@ -605,11 +912,18 @@ export const NiceSelect = styled.div`
appearance: none;
outline: 0;
box-shadow: none;
- background-image: none;
+
+ background-image: ${image};
+ background-position: right 4px center;
+ background-repeat: no-repeat;
+ background-size: 32px 32px;
+
background-color: white;
- flex: 1;
- padding: 0.5em 1em;
+ border-radius: 0.25rem;
+ font-size: 1em;
+ padding: 8px 32px 8px 8px;
+ /* 0.5em 3em 0.5em 1em; */
cursor: pointer;
}
@@ -617,29 +931,8 @@ export const NiceSelect = styled.div`
display: flex;
/* width: 10em; */
overflow: hidden;
- border-radius: .25em;
-
- &::after {
- content: '\u25BC';
- position: absolute;
- top: 0;
- right: 0;
- padding: 0.5em 1em;
- cursor: pointer;
- pointer-events: none;
- -webkit-transition: .25s all ease;
- -o-transition: .25s all ease;
- transition: .25s all ease;
- }
-
- &:hover::after {
- /* color: #f39c12; */
- }
-
- &::-ms-expand {
- display: none;
- }
-`
+ border-radius: 0.25em;
+`;
export const Outlined = styled.div`
border: 2px solid #388e3c;
@@ -647,13 +940,12 @@ export const Outlined = styled.div`
width: fit-content;
border-radius: 2px;
color: #388e3c;
-`
+`;
/* { width: "1.5em", height: "1.5em", verticalAlign: "middle" } */
export const CheckboxSuccess = styled.input`
vertical-align: center;
-
-`
+`;
export const TermsSection = styled.a`
border: 1px solid black;
@@ -664,13 +956,13 @@ export const TermsSection = styled.a`
text-decoration: none;
color: inherit;
flex-direction: column;
-
+
display: flex;
&[data-open="true"] {
- display: flex;
+ display: flex;
}
&[data-open="false"] > *:not(:first-child) {
- display: none;
+ display: none;
}
header {
@@ -681,15 +973,15 @@ export const TermsSection = styled.a`
height: auto;
}
- &[data-open="true"] header:after {
- content: '\\2227';
+ &[data-open="true"] header:after {
+ content: "\\2227";
}
- &[data-open="false"] header:after {
- content: '\\2228';
+ &[data-open="false"] header:after {
+ content: "\\2228";
}
`;
-export const TermsOfService = styled.div`
+export const TermsOfServiceStyle = styled.div`
display: flex;
flex-direction: column;
text-align: left;
@@ -712,13 +1004,13 @@ export const TermsOfService = styled.div`
padding: 1em;
margin-top: 2px;
margin-bottom: 2px;
-
+
display: flex;
&[data-open="true"] {
- display: flex;
+ display: flex;
}
&[data-open="false"] > *:not(:first-child) {
- display: none;
+ display: none;
}
header {
@@ -729,27 +1021,27 @@ export const TermsOfService = styled.div`
height: auto;
}
- &[data-open="true"] > header:after {
- content: '\\2227';
+ &[data-open="true"] > header:after {
+ content: "\\2227";
}
- &[data-open="false"] > header:after {
- content: '\\2228';
+ &[data-open="false"] > header:after {
+ content: "\\2228";
}
}
-
-`
+`;
export const StyledCheckboxLabel = styled.div`
color: green;
text-transform: uppercase;
/* font-weight: bold; */
text-align: center;
+ cursor: pointer;
span {
-
input {
display: none;
opacity: 0;
width: 1em;
height: 1em;
+ cursor: pointer;
}
div {
display: inline-grid;
@@ -758,7 +1050,7 @@ export const StyledCheckboxLabel = styled.div`
margin-right: 1em;
border-radius: 2px;
border: 2px solid currentColor;
-
+
svg {
transition: transform 0.1s ease-in 25ms;
transform: scale(0);
@@ -768,6 +1060,7 @@ export const StyledCheckboxLabel = styled.div`
label {
padding: 0px;
font-size: small;
+ cursor: pointer;
}
}
@@ -776,12 +1069,25 @@ export const StyledCheckboxLabel = styled.div`
}
input:disabled + div {
color: #959495;
- };
+ }
input:disabled + div + label {
color: #959495;
- };
+ }
input:focus + div + label {
box-shadow: 0 0 0 0.05em #fff, 0 0 0.15em 0.1em currentColor;
}
+`;
-` \ No newline at end of file
+export const ParagraphClickable = styled.p`
+ cursor: pointer;
+ margin: 0px;
+ padding: 8px 16px;
+ :hover {
+ filter: alpha(opacity=80);
+ background-image: linear-gradient(
+ transparent,
+ rgba(0, 0, 0, 0.1) 40%,
+ rgba(0, 0, 0, 0.2)
+ );
+ }
+`;
diff --git a/packages/taler-wallet-webextension/src/context/alert.ts b/packages/taler-wallet-webextension/src/context/alert.ts
new file mode 100644
index 000000000..e30fdd72c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/context/alert.ts
@@ -0,0 +1,277 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext, useState } from "preact/hooks";
+import { HookError } from "../hooks/useAsyncAsHook.js";
+import { SafeHandler, withSafe } from "../mui/handlers.js";
+import { BackgroundError } from "../wxApi.js";
+import {
+ InternationalizationAPI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { platform } from "../platform/foreground.js";
+
+export type AlertType = "info" | "warning" | "error" | "success";
+
+export interface InfoAlert {
+ message: TranslatedString;
+ description: TranslatedString | VNode;
+ type: "info" | "warning" | "success";
+}
+
+export type Alert = InfoAlert | ErrorAlert;
+
+export interface ErrorAlert {
+ message: TranslatedString;
+ description: TranslatedString | VNode;
+ type: "error";
+ context: object | undefined;
+ cause: any | undefined;
+}
+
+type Type = {
+ alerts: Alert[];
+ pushAlert: (n: Alert) => void;
+ removeAlert: (n: Alert) => void;
+ /**
+ *
+ * @param h
+ * @returns
+ * @deprecated use safely
+ */
+ pushAlertOnError: <T>(h: (p: T) => Promise<void>) => SafeHandler<T>;
+ safely: <T>(name: string, h: (p: T) => Promise<void>) => SafeHandler<T>;
+};
+
+const initial: Type = {
+ alerts: [],
+ pushAlertOnError: () => {
+ throw Error("alert context not initialized");
+ },
+ safely: () => {
+ throw Error("alert context not initialized");
+ },
+ pushAlert: () => {
+ null;
+ },
+ removeAlert: () => {
+ null;
+ },
+};
+
+const Context = createContext<Type>(initial);
+
+type AlertWithDate = Alert & { since: Date };
+
+type Props = Partial<Type> & {
+ children: ComponentChildren;
+};
+
+export const AlertProvider = ({ children }: Props): VNode => {
+ const timeout = 3000;
+
+ const [alerts, setAlerts] = useState<AlertWithDate[]>([]);
+
+ const pushAlert = (n: Alert): void => {
+ const entry = { ...n, since: new Date() };
+ setAlerts((ns) => [...ns, entry]);
+ if (n.type !== "error") {
+ setTimeout(() => {
+ setAlerts((ns) => ns.filter((x) => x.since !== entry.since));
+ }, timeout);
+ }
+ };
+
+ const removeAlert = (alert: Alert): void => {
+ setAlerts((ns: AlertWithDate[]) => ns.filter((n) => n !== alert));
+ };
+
+ const { i18n } = useTranslationContext();
+
+ function pushAlertOnError<T>(
+ handler: (p: T) => Promise<void>,
+ ): SafeHandler<T> {
+ return withSafe(handler, (e) => {
+ const a = alertFromError(i18n, e.message as TranslatedString, e);
+ pushAlert(a);
+ });
+ }
+
+ function safely<T>(
+ name: string,
+ handler: (p: T) => Promise<void>,
+ ): SafeHandler<T> {
+ const message = i18n.str`Error was thrown trying to: "${name}"`;
+ return withSafe(handler, (e) => {
+ const a = alertFromError(i18n, message, e);
+ pushAlert(a);
+ });
+ }
+
+ return h(Context.Provider, {
+ value: { alerts, pushAlert, removeAlert, pushAlertOnError, safely },
+ children,
+ });
+};
+
+export const useAlertContext = (): Type => useContext(Context);
+
+export function alertFromError(
+ i18n: InternationalizationAPI,
+ message: TranslatedString,
+ error: HookError,
+ ...context: any[]
+): ErrorAlert;
+
+export function alertFromError(
+ i18n: InternationalizationAPI,
+ message: TranslatedString,
+ error: Error,
+ ...context: any[]
+): ErrorAlert;
+
+export function alertFromError(
+ i18n: InternationalizationAPI,
+ message: TranslatedString,
+ error: TalerErrorDetail,
+ ...context: any[]
+): ErrorAlert;
+
+export function alertFromError(
+ i18n: InternationalizationAPI,
+ message: TranslatedString,
+ error: HookError | TalerErrorDetail | Error,
+ ...context: any[]
+): ErrorAlert {
+ let description: TranslatedString;
+ let cause: any;
+
+ if (typeof error === "object" && error !== null) {
+ if ("code" in error) {
+ //TalerErrorDetail
+ description = (error.hint ??
+ `Error code: ${error.code}`) as TranslatedString;
+ cause = {
+ details: error,
+ };
+ } else if ("hasError" in error) {
+ //HookError
+ description = error.message as TranslatedString;
+ if (error.type === "taler") {
+ const msg = isWalletNotAvailable(i18n, error.details);
+ if (msg) {
+ description = msg;
+ } else {
+ const msg2 = isHttpError(i18n, error.details);
+ if (msg2) {
+ description = msg2;
+ }
+ }
+ cause = {
+ details: error.details,
+ };
+ }
+ } else {
+ if (error instanceof BackgroundError) {
+ const msg = isWalletNotAvailable(i18n, error.errorDetail);
+ if (msg) {
+ description = msg;
+ } else {
+ const msg2 = isHttpError(i18n, error.errorDetail);
+ if (msg2) {
+ description = msg2;
+ } else {
+ description = (error.errorDetail.hint ??
+ `Error code: ${error.errorDetail.code}`) as TranslatedString;
+ }
+ }
+ cause = {
+ details: error.errorDetail,
+ stack: error.stack,
+ };
+ } else {
+ description = error.message as TranslatedString;
+ cause = {
+ stack: error.stack,
+ };
+ }
+ }
+ } else {
+ description = "" as TranslatedString;
+ cause = error;
+ }
+
+ return {
+ type: "error",
+ message,
+ description,
+ cause,
+ context,
+ };
+}
+
+function isWalletNotAvailable(
+ i18n: InternationalizationAPI,
+ detail: TalerErrorDetail,
+): TranslatedString | undefined {
+ if (
+ detail.code === TalerErrorCode.WALLET_CORE_NOT_AVAILABLE &&
+ detail.lastError
+ ) {
+ const le = detail.lastError as TalerErrorDetail;
+ if (le.code === TalerErrorCode.WALLET_DB_UNAVAILABLE) {
+ if (platform.isFirefox() && platform.runningOnPrivateMode()) {
+ return i18n.str`Could not open the wallet database. Firefox is known to run into this problem under "permanent private mode".`;
+ } else {
+ return i18n.str`Could not open the wallet database.`;
+ }
+ } else {
+ return (detail.hint ?? `Error code: ${detail.code}`) as TranslatedString;
+ }
+ }
+ return undefined;
+}
+
+function isHttpError(
+ i18n: InternationalizationAPI,
+ detail: TalerErrorDetail,
+): TranslatedString | undefined {
+ if (
+ detail.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
+ detail.errorResponse
+ ) {
+ const er = detail.errorResponse as TalerErrorDetail;
+ return (
+ (er.hint as TranslatedString) ??
+ detail.hint ??
+ i18n.str`Unexpected request error, code: ${er.code}`
+ );
+ }
+ return undefined;
+}
+//
diff --git a/packages/taler-wallet-webextension/src/context/backend.ts b/packages/taler-wallet-webextension/src/context/backend.ts
new file mode 100644
index 000000000..280fb266d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/context/backend.ts
@@ -0,0 +1,52 @@
+/*
+ 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 { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { wxApi, WxApiType } from "../wxApi.js";
+
+type Type = WxApiType;
+
+const initial = wxApi;
+
+const Context = createContext<Type>(initial);
+
+type Props = Partial<Type> & {
+ children: ComponentChildren;
+};
+
+export const BackendProvider = ({
+ wallet,
+ background,
+ listener,
+ children,
+}: Props): VNode => {
+ return h(Context.Provider, {
+ value: {
+ wallet: wallet ?? initial.wallet,
+ background: background ?? initial.background,
+ listener: listener ?? initial.listener,
+ },
+ children,
+ });
+};
+
+export const useBackendContext = (): Type => useContext(Context);
diff --git a/packages/taler-wallet-webextension/src/context/devContext.ts b/packages/taler-wallet-webextension/src/context/devContext.ts
deleted file mode 100644
index ea2ba4ceb..000000000
--- a/packages/taler-wallet-webextension/src/context/devContext.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createContext, h, VNode } from 'preact'
-import { useContext, useState } from 'preact/hooks'
-import { useLocalStorage } from '../hooks/useLocalStorage';
-
-interface Type {
- devMode: boolean;
- toggleDevMode: () => void;
-}
-const Context = createContext<Type>({
- devMode: false,
- toggleDevMode: () => null
-})
-
-export const useDevContext = (): Type => useContext(Context);
-
-export const DevContextProvider = ({ children }: { children: any }): VNode => {
- const [value, setter] = useLocalStorage('devMode')
- const devMode = value === "true"
- const toggleDevMode = () => setter(v => !v ? "true" : undefined)
- return h(Context.Provider, { value: { devMode, toggleDevMode }, children });
-}
diff --git a/packages/taler-wallet-webextension/src/context/iocContext.ts b/packages/taler-wallet-webextension/src/context/iocContext.ts
new file mode 100644
index 000000000..89f984f2f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/context/iocContext.ts
@@ -0,0 +1,67 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { platform } from "../platform/foreground.js";
+
+interface Type {
+ findTalerUriInActiveTab: () => Promise<string | undefined>;
+ findTalerUriInClipboard: () => Promise<string | undefined>;
+}
+const Context = createContext<Type>({
+ findTalerUriInActiveTab: async () => undefined,
+ findTalerUriInClipboard: async () => undefined,
+});
+
+/**
+ * Inversion of control Context
+ *
+ * This context act as a proxy between API that need to be replaced in
+ * different environments
+ *
+ * @returns
+ */
+export const useIocContext = (): Type => useContext(Context);
+
+export const IoCProviderForTesting = ({
+ value,
+ children,
+}: {
+ value: Type;
+ children: any;
+}): VNode => {
+ return h(Context.Provider, { value, children });
+};
+
+export const IoCProviderForRuntime = ({
+ children,
+}: {
+ children: any;
+}): VNode => {
+ return h(Context.Provider, {
+ value: {
+ findTalerUriInActiveTab: platform.findTalerUriInActiveTab,
+ findTalerUriInClipboard: platform.findTalerUriInClipboard,
+ },
+ children,
+ });
+};
diff --git a/packages/taler-wallet-webextension/src/context/translation.ts b/packages/taler-wallet-webextension/src/context/translation.ts
deleted file mode 100644
index 5f57958de..000000000
--- a/packages/taler-wallet-webextension/src/context/translation.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createContext, h, VNode } from 'preact'
-import { useContext, useEffect } from 'preact/hooks'
-import { useLang } from '../hooks/useLang'
-//@ts-ignore: type declaration
-import * as jedLib from "jed";
-import { strings } from "../i18n/strings";
-import { setupI18n } from '@gnu-taler/taler-util';
-
-interface Type {
- lang: string;
- changeLanguage: (l: string) => void;
-}
-const initial = {
- lang: 'en',
- changeLanguage: () => {
- // do not change anything
- }
-}
-const Context = createContext<Type>(initial)
-
-interface Props {
- initial?: string,
- children: any,
- forceLang?: string
-}
-
-//we use forceLang when we don't want to use the saved state, but sone forced
-//runtime lang predefined lang
-export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => {
- const [lang, changeLanguage] = useLang(initial)
- useEffect(() => {
- if (forceLang) {
- changeLanguage(forceLang)
- }
- })
- useEffect(()=> {
- setupI18n(lang, strings)
- },[lang])
- if (forceLang) {
- setupI18n(forceLang, strings)
- } else {
- setupI18n(lang, strings)
- }
- return h(Context.Provider, { value: { lang, changeLanguage }, children });
-}
-
-export const useTranslationContext = (): Type => useContext(Context);
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/index.ts b/packages/taler-wallet-webextension/src/cta/Deposit/index.ts
new file mode 100644
index 000000000..6b228188b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/index.ts
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AmountJson, AmountString } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+
+export interface Props {
+ talerDepositUri: string | undefined;
+ amountStr: AmountString | undefined;
+ cancel: () => Promise<void>;
+ onSuccess: (tx: string) => Promise<void>;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+ export interface Ready {
+ status: "ready";
+ error: undefined;
+ fee: AmountJson;
+ cost: AmountJson;
+ effective: AmountJson;
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
+ }
+ export interface Completed {
+ status: "completed";
+ error: undefined;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ ready: ReadyView,
+};
+
+export const DepositPage = compose(
+ "Deposit",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/state.ts b/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
new file mode 100644
index 000000000..efcef8c28
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/state.ts
@@ -0,0 +1,79 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerDepositUri,
+ amountStr,
+ cancel,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const info = useAsyncAsHook(async () => {
+ if (!talerDepositUri) throw Error("ERROR_NO-URI-FOR-DEPOSIT");
+ if (!amountStr) throw Error("ERROR_NO-AMOUNT-FOR-DEPOSIT");
+ const amount = Amounts.parse(amountStr);
+ if (!amount) throw Error("ERROR_INVALID-AMOUNT-FOR-DEPOSIT");
+ const deposit = await api.wallet.call(WalletApiOperation.PrepareDeposit, {
+ amount: Amounts.stringify(amount),
+ depositPaytoUri: talerDepositUri,
+ });
+ return { deposit, uri: talerDepositUri, amount };
+ });
+ const { i18n } = useTranslationContext();
+
+ if (!info) return { status: "loading", error: undefined };
+ if (info.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the status of deposit`,
+ info,
+ ),
+ };
+ }
+
+ const { deposit, uri, amount } = info.response;
+ async function doDeposit(): Promise<void> {
+ const resp = await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
+ amount: Amounts.stringify(amount),
+ depositPaytoUri: uri,
+ });
+ onSuccess(resp.transactionId);
+ }
+
+ return {
+ status: "ready",
+ error: undefined,
+ confirm: {
+ onClick: pushAlertOnError(doDeposit),
+ },
+ fee: Amounts.sub(deposit.totalDepositCost, deposit.effectiveDepositAmount)
+ .amount,
+ cost: Amounts.parseOrThrow(deposit.totalDepositCost),
+ effective: Amounts.parseOrThrow(deposit.effectiveDepositAmount),
+ cancel,
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx b/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
new file mode 100644
index 000000000..cd65ce8e1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/stories.tsx
@@ -0,0 +1,37 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "deposit",
+};
+
+export const Ready = tests.createExample(ReadyView, {
+ status: "ready",
+ confirm: {},
+ cost: Amounts.parseOrThrow("EUR:1.2"),
+ effective: Amounts.parseOrThrow("EUR:1"),
+ fee: Amounts.parseOrThrow("EUR:0.2"),
+ error: undefined,
+});
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/test.ts b/packages/taler-wallet-webextension/src/cta/Deposit/test.ts
new file mode 100644
index 000000000..100929918
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/test.ts
@@ -0,0 +1,118 @@
+/*
+ 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 { AmountString, Amounts } from "@gnu-taler/taler-util";
+import { expect } from "chai";
+import { createWalletApiMock } from "../../test-utils.js";
+import { useComponentState } from "./state.js";
+import * as tests from "@gnu-taler/web-util/testing";
+import { Props } from "./index.js";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+describe("Deposit CTA states", () => {
+ it("should tell the user that the URI is missing", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ const props: Props = {
+ talerDepositUri: undefined,
+ amountStr: undefined,
+ cancel: async () => {
+ null;
+ },
+ onSuccess: async () => {
+ null;
+ },
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ ({ status, error }) => {
+ expect(status).equals("error");
+
+ if (!error) expect.fail();
+ // if (!error.hasError) expect.fail();
+ // if (error.operational) expect.fail();
+ expect(error.description).eq("ERROR_NO-URI-FOR-DEPOSIT");
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should be ready after loading", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PrepareDeposit,
+ undefined,
+ {
+ effectiveDepositAmount: "EUR:1" as AmountString,
+ totalDepositCost: "EUR:1.2" as AmountString,
+ fees: {
+ coin: "EUR:0" as AmountString,
+ refresh: "EUR:0.2" as AmountString,
+ wire: "EUR:0" as AmountString,
+ },
+ },
+ );
+
+ const props = {
+ talerDepositUri: "payto://refund/asdasdas",
+ amountStr: "EUR:1" as AmountString,
+ cancel: async () => {
+ null;
+ },
+ onSuccess: async () => {
+ null;
+ },
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.confirm.onClick).not.undefined;
+ expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:1.2"));
+ expect(state.fee).deep.eq(Amounts.parseOrThrow("EUR:0.2"));
+ expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:1"));
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx b/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
new file mode 100644
index 000000000..c683a755c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Deposit/views.tsx
@@ -0,0 +1,72 @@
+/*
+ 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 ReadyView(state: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <section>
+ {Amounts.isNonZero(state.cost) && (
+ <Part
+ big
+ title={i18n.str`Cost`}
+ text={<Amount value={state.cost} />}
+ kind="negative"
+ />
+ )}
+ {Amounts.isNonZero(state.fee) && (
+ <Part
+ big
+ title={i18n.str`Fee`}
+ text={<Amount value={state.fee} />}
+ kind="negative"
+ />
+ )}
+ <Part
+ big
+ title={i18n.str`To be received`}
+ text={<Amount value={state.effective} />}
+ kind="positive"
+ />
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.confirm.onClick}
+ >
+ <i18n.Translate>
+ Send &nbsp; {<Amount value={state.cost} />}
+ </i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts
new file mode 100644
index 000000000..ec09fd9f1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/index.ts
@@ -0,0 +1,73 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { InsertLostView, InsertPendingRefreshView, UnknownView } from "./views.js";
+
+export interface Props {
+ talerExperimentUri: string | undefined;
+ onCancel: () => Promise<void>;
+ onSuccess: () => Promise<void>;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Unknown | State.InsertLost | State.PendingRefresh;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+ export interface InsertLost {
+ status: "insertLost";
+ error: undefined;
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
+ }
+ export interface PendingRefresh {
+ status: "pendingRefresh";
+ error: undefined;
+ confirm: ButtonHandler;
+ cancel: () => Promise<void>;
+ }
+ export interface Unknown {
+ status: "unknown";
+ experimentId: string;
+ error: undefined;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ pendingRefresh: InsertPendingRefreshView,
+ insertLost: InsertLostView,
+ unknown: UnknownView,
+};
+
+export const DevExperimentPage = compose(
+ "DevExperiment",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts
new file mode 100644
index 000000000..774a1129d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/state.ts
@@ -0,0 +1,83 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { parseDevExperimentUri } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerExperimentUri,
+ onCancel,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
+
+ async function doApply(): Promise<void> {
+ if (!talerExperimentUri) return;
+ await api.wallet.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: talerExperimentUri
+ })
+ // const resp = await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
+ // amount: Amounts.stringify(amount),
+ // depositPaytoUri: uri,
+ // });
+ onSuccess();
+ }
+ const uri = talerExperimentUri === undefined ? undefined : parseDevExperimentUri(talerExperimentUri);
+
+ if (!uri) {
+ return {
+ status: "error",
+ error: {
+ type: "error",
+ message: i18n.str`Invalid dev experiment URI.`,
+ description: i18n.str`URI: ${talerExperimentUri}`,
+ cause: {},
+ context: {},
+ },
+ };
+ }
+ if (uri.devExperimentId === "insert-denom-loss") {
+ return {
+ status: "insertLost",
+ error: undefined,
+ confirm: {
+ onClick: pushAlertOnError(doApply),
+ },
+ cancel: onCancel,
+ };
+ }
+ if (uri.devExperimentId === "insert-pending-refresh") {
+ return {
+ status: "pendingRefresh",
+ error: undefined,
+ confirm: {
+ onClick: pushAlertOnError(doApply),
+ },
+ cancel: onCancel,
+ };
+ }
+ return {
+ status: "unknown",
+ error: undefined,
+ experimentId: uri.devExperimentId,
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx b/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx
new file mode 100644
index 000000000..c9851495f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/stories.tsx
@@ -0,0 +1,33 @@
+/*
+ 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 { InsertLostView } from "./views.js";
+
+export default {
+ title: "dev-experiment",
+};
+
+export const Ready = tests.createExample(InsertLostView, {
+ status: "insertLost",
+ confirm: {},
+ error: undefined,
+});
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts b/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts
new file mode 100644
index 000000000..d4f2ca8b1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/test.ts
@@ -0,0 +1,65 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { createWalletApiMock } from "../../test-utils.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+
+describe("DevExperiment CTA states", () => {
+ it("should tell the user that the URI is missing", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ const props: Props = {
+ talerExperimentUri: undefined,
+ onCancel: async () => {
+ null;
+ },
+ onSuccess: async () => {
+ null;
+ },
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("error");
+ },
+ ({ status, error }) => {
+ expect(status).equals("error");
+
+ if (!error) expect.fail();
+ // if (!error.hasError) expect.fail();
+ // if (error.operational) expect.fail();
+ // expect(error.description).eq("ERROR_NO-URI-FOR-DEPOSIT");
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+});
diff --git a/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx b/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx
new file mode 100644
index 000000000..afad17ad1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/DevExperiment/views.tsx
@@ -0,0 +1,74 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Amount } from "../../components/Amount.js";
+import { Part } from "../../components/Part.js";
+import { Button } from "../../mui/Button.js";
+import { State } from "./index.js";
+
+/**
+ *
+ * @author sebasjm
+ */
+
+export function InsertLostView(state: State.InsertLost): VNode {
+ const { i18n } = useTranslationContext();
+ return <Fragment>
+ <section>
+ <Part
+ title={i18n.str`Experiment`}
+ text={i18n.str`Insert lost denomination`}
+ />
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.confirm.onClick}
+ >
+ <i18n.Translate>Apply</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+}
+
+export function InsertPendingRefreshView(state: State.PendingRefresh): VNode {
+ const { i18n } = useTranslationContext();
+ return <Fragment>
+ <section>
+ <Part
+ title={i18n.str`Experiment`}
+ text={i18n.str`Pending refresh`}
+ />
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.confirm.onClick}
+ >
+ <i18n.Translate>Apply</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+}
+
+export function UnknownView(state: State.Unknown): VNode {
+ return <div>unknown experiment "{state.experimentId}"</div>
+}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
new file mode 100644
index 000000000..fd3fb52f8
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AmountJson, AmountString } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
+import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
+import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+
+export interface Props {
+ amount: AmountString;
+ onClose: () => Promise<void>;
+ onSuccess: (tx: string) => Promise<void>;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | State.Ready
+ | SelectExchangeState.Selecting
+ | SelectExchangeState.NoExchangeFound;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ cancel: ButtonHandler;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ doSelectExchange: ButtonHandler;
+ create: ButtonHandler;
+ subject: TextFieldHandler;
+ expiration: TextFieldHandler;
+ toBeReceived: AmountJson;
+ requestAmount: AmountJson;
+ exchangeUrl: string;
+ error: undefined;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ "no-exchange-found": NoExchangesView,
+ "selecting-exchange": ExchangeSelectionPage,
+ ready: ReadyView,
+};
+
+export const InvoiceCreatePage = compose(
+ "InvoiceCreatePage",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
new file mode 100644
index 000000000..daa3ee76d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
@@ -0,0 +1,196 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/* eslint-disable react-hooks/rules-of-hooks */
+import { Amounts, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { isFuture, parse } from "date-fns";
+import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
+import { RecursiveState } from "../../utils/index.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ amount: amountStr,
+ onClose,
+ onSuccess,
+}: Props): RecursiveState<State> {
+ const amount = Amounts.parseOrThrow(amountStr);
+ const api = useBackendContext();
+
+ const hook = useAsyncAsHook(() =>
+ api.wallet.call(WalletApiOperation.ListExchanges, {}),
+ );
+ const { i18n } = useTranslationContext();
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the list of exchanges`,
+ hook,
+ ),
+ };
+ }
+ // if (hook.hasError) {
+ // return {
+ // status: "loading-uri",
+ // error: hook,
+ // };
+ // }
+
+ const exchangeList = hook.response.exchanges;
+
+ return () => {
+ const [subject, setSubject] = useState<string | undefined>();
+ const [timestamp, setTimestamp] = useState<string | undefined>();
+ const { pushAlertOnError } = useAlertContext();
+
+ const selectedExchange = useSelectedExchange({
+ currency: amount.currency,
+ defaultExchange: undefined,
+ list: exchangeList,
+ });
+
+ if (selectedExchange.status !== "ready") {
+ return selectedExchange;
+ }
+
+ const exchange = selectedExchange.selected;
+
+ const hook = useAsyncAsHook(async () => {
+ const resp = await api.wallet.call(
+ WalletApiOperation.CheckPeerPullCredit,
+ {
+ amount: amountStr,
+ exchangeBaseUrl: exchange.exchangeBaseUrl,
+ },
+ );
+ return resp;
+ });
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the invoice status`,
+ hook,
+ ),
+ };
+ // return {
+ // status: "loading-uri",
+ // error: hook,
+ // };
+ }
+
+ const { amountEffective, amountRaw } = hook.response;
+ const requestAmount = Amounts.parseOrThrow(amountRaw);
+ const toBeReceived = Amounts.parseOrThrow(amountEffective);
+
+ let purse_expiration: TalerProtocolTimestamp | undefined = undefined;
+ let timestampError: string | undefined = undefined;
+
+ const t =
+ timestamp === undefined
+ ? undefined
+ : parse(timestamp, "dd/MM/yyyy", new Date());
+
+ if (t !== undefined) {
+ if (Number.isNaN(t.getTime())) {
+ timestampError = 'Should have the format "dd/MM/yyyy"';
+ } else {
+ if (!isFuture(t)) {
+ timestampError = "Should be in the future";
+ } else {
+ purse_expiration = {
+ t_s: t.getTime() / 1000,
+ };
+ }
+ }
+ }
+
+ async function accept(): Promise<void> {
+ if (!subject || !purse_expiration) return;
+
+ const resp = await api.wallet.call(
+ WalletApiOperation.InitiatePeerPullCredit,
+ {
+ exchangeBaseUrl: exchange.exchangeBaseUrl,
+ partialContractTerms: {
+ amount: Amounts.stringify(amount),
+ summary: subject,
+ purse_expiration,
+ },
+ },
+ );
+
+ onSuccess(resp.transactionId);
+ }
+ const unableToCreate =
+ !subject || Amounts.isZero(amount) || !purse_expiration;
+
+ return {
+ status: "ready",
+ subject: {
+ error:
+ subject === undefined
+ ? undefined
+ : !subject
+ ? "Can't be empty"
+ : undefined,
+ value: subject ?? "",
+ onInput: pushAlertOnError(async (e) => setSubject(e)),
+ },
+ expiration: {
+ error: timestampError,
+ value: timestamp === undefined ? "" : timestamp,
+ onInput: pushAlertOnError(async (e) => {
+ setTimestamp(e);
+ }),
+ },
+ doSelectExchange: selectedExchange.doSelect,
+ exchangeUrl: exchange.exchangeBaseUrl,
+ create: {
+ onClick: unableToCreate ? undefined : pushAlertOnError(accept),
+ },
+ cancel: {
+ onClick: pushAlertOnError(onClose),
+ },
+ requestAmount,
+ toBeReceived,
+ error: undefined,
+ };
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
new file mode 100644
index 000000000..779f130aa
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
@@ -0,0 +1,52 @@
+/*
+ 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 { nullFunction } from "../../mui/handlers.js";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "invoice create",
+};
+
+export const Ready = tests.createExample(ReadyView, {
+ requestAmount: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ },
+ expiration: {
+ value: "2/12/12",
+ },
+ cancel: {},
+ toBeReceived: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ },
+ doSelectExchange: {},
+ exchangeUrl: "https://exchange.taler.ar",
+ subject: {
+ value: "some subject",
+ onInput: nullFunction,
+ },
+ create: {},
+});
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/test.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/test.ts
new file mode 100644
index 000000000..3ebedfd5a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/test.ts
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("Invoice create state", () => {
+ it.skip("should create some tests", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
new file mode 100644
index 000000000..e2c37fbba
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
@@ -0,0 +1,155 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { Part } from "../../components/Part.js";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
+import { Button } from "../../mui/Button.js";
+import { TextField } from "../../mui/TextField.js";
+import {
+ ExchangeDetails,
+ getAmountWithFee,
+ InvoiceCreationDetails,
+} from "../../wallet/Transaction.js";
+import { State } from "./index.js";
+
+export function ReadyView({
+ exchangeUrl,
+ subject,
+ expiration,
+ create,
+ toBeReceived,
+ requestAmount,
+ doSelectExchange: _doSelectExchange,
+}: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+
+ async function oneDayExpiration(): Promise<void> {
+ if (expiration.onInput) {
+ expiration.onInput(
+ format(new Date().getTime() + 1000 * 60 * 60 * 24, "dd/MM/yyyy"),
+ );
+ }
+ }
+
+ async function oneWeekExpiration(): Promise<void> {
+ if (expiration.onInput) {
+ expiration.onInput(
+ format(new Date().getTime() + 1000 * 60 * 60 * 24 * 7, "dd/MM/yyyy"),
+ );
+ }
+ }
+ async function _30DaysExpiration(): Promise<void> {
+ if (expiration.onInput) {
+ expiration.onInput(
+ format(new Date().getTime() + 1000 * 60 * 60 * 24 * 30, "dd/MM/yyyy"),
+ );
+ }
+ }
+ return (
+ <Fragment>
+ <section style={{ textAlign: "left" }}>
+ <Part
+ title={
+ <div
+ style={{
+ display: "flex",
+ alignItems: "center",
+ }}
+ >
+ <i18n.Translate>Exchange</i18n.Translate>
+ {/* <Button onClick={doSelectExchange.onClick} variant="text">
+ <SvgIcon
+ title="Edit"
+ dangerouslySetInnerHTML={{ __html: editIcon }}
+ color="black"
+ />
+ </Button> */}
+ </div>
+ }
+ text={<ExchangeDetails exchange={exchangeUrl} />}
+ kind="neutral"
+ big
+ />
+ <p>
+ <TextField
+ label="Subject"
+ variant="filled"
+ error={subject.error}
+ helperText={i18n.str`Short description of the invoice`}
+ required
+ fullWidth
+ value={subject.value}
+ onChange={subject.onInput}
+ />
+ </p>
+
+ <p>
+ <TextField
+ label="Expiration"
+ variant="filled"
+ error={expiration.error}
+ required
+ fullWidth
+ value={expiration.value}
+ onChange={expiration.onInput}
+ />
+ <p>
+ <Button
+ variant="outlined"
+ disabled={!expiration.onInput}
+ onClick={oneDayExpiration}
+ >
+ 1 day
+ </Button>
+ <Button
+ variant="outlined"
+ disabled={!expiration.onInput}
+ onClick={oneWeekExpiration}
+ >
+ 1 week
+ </Button>
+ <Button
+ variant="outlined"
+ disabled={!expiration.onInput}
+ onClick={_30DaysExpiration}
+ >
+ 30 days
+ </Button>
+ </p>
+ </p>
+
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <InvoiceCreationDetails
+ amount={getAmountWithFee(toBeReceived, requestAmount, "credit")}
+ />
+ }
+ />
+ </section>
+ <section>
+ <TermsOfService key="terms" exchangeUrl={exchangeUrl} >
+ <Button onClick={create.onClick} variant="contained" color="success">
+ <i18n.Translate>Create</i18n.Translate>
+ </Button>
+ </TermsOfService>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
new file mode 100644
index 000000000..f0cd63fbe
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
@@ -0,0 +1,97 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ PreparePayResult,
+ TalerErrorDetail,
+} from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+
+export interface Props {
+ talerPayPullUri: string;
+ onClose: () => Promise<void>;
+ goToWalletManualWithdraw: (amount?: string) => Promise<void>;
+ onSuccess: (tx: string) => Promise<void>;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | State.NoEnoughBalance
+ | State.NoBalanceForCurrency
+ | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ uri: string;
+ cancel: ButtonHandler;
+ effective: AmountJson;
+ raw: AmountJson;
+ goToWalletManualWithdraw: (currency: string) => Promise<void>;
+ summary: string | undefined;
+ expiration: AbsoluteTime | undefined;
+ payStatus: PreparePayResult;
+ }
+
+ export interface NoBalanceForCurrency extends BaseInfo {
+ status: "no-balance-for-currency";
+ balance: undefined;
+ }
+ export interface NoEnoughBalance extends BaseInfo {
+ status: "no-enough-balance";
+ balance: AmountJson;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ balance: AmountJson;
+ accept: ButtonHandler;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ "no-balance-for-currency": ReadyView,
+ "no-enough-balance": ReadyView,
+ ready: ReadyView,
+};
+
+export const InvoicePayPage = compose(
+ "InvoicePayPage",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
new file mode 100644
index 000000000..99de03d2d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
@@ -0,0 +1,171 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ Amounts,
+ NotificationType,
+ PreparePayResult,
+ PreparePayResultType,
+ TalerProtocolTimestamp,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useEffect } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerPayPullUri,
+ onClose,
+ goToWalletManualWithdraw,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const { pushAlertOnError } = useAlertContext();
+ const hook = useAsyncAsHook(async () => {
+ const p2p = await api.wallet.call(WalletApiOperation.PreparePeerPullDebit, {
+ talerUri: talerPayPullUri,
+ });
+ const balance = await api.wallet.call(WalletApiOperation.GetBalances, {});
+ return { p2p, balance };
+ });
+
+ useEffect(() =>
+ api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
+ hook?.retry,
+ ),
+ );
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the transfer payment status`,
+ hook,
+ ),
+ };
+ }
+ // if (hook.hasError) {
+ // return {
+ // status: "loading-uri",
+ // error: hook,
+ // };
+ // }
+
+ const { contractTerms, transactionId, amountEffective, amountRaw } =
+ hook.response.p2p;
+
+ const amountStr: string = contractTerms.amount;
+ const amount = Amounts.parseOrThrow(amountStr);
+ const effective = Amounts.parseOrThrow(amountEffective);
+ const raw = Amounts.parseOrThrow(amountRaw);
+ const summary: string | undefined = contractTerms.summary;
+ const expiration: TalerProtocolTimestamp | undefined =
+ contractTerms.purse_expiration;
+
+ const foundBalance = hook.response.balance.balances.find(
+ (b) => Amounts.parseOrThrow(b.available).currency === amount.currency,
+ );
+
+ const paymentPossible: PreparePayResult = {
+ status: PreparePayResultType.PaymentPossible,
+ proposalId: "fakeID",
+ contractTerms: {} as any,
+ contractTermsHash: "asd",
+ amountRaw: hook.response.p2p.amount,
+ amountEffective: hook.response.p2p.amount,
+ } as PreparePayResult;
+
+ const insufficientBalance: PreparePayResult = {
+ status: PreparePayResultType.InsufficientBalance,
+ talerUri: "taler://pay",
+ proposalId: "fakeID",
+ contractTerms: {} as any,
+ amountRaw: hook.response.p2p.amount,
+ noncePriv: "",
+ } as any; //FIXME: check this interface with new values
+
+ const baseResult = {
+ uri: talerPayPullUri,
+ cancel: {
+ onClick: pushAlertOnError(onClose),
+ },
+ effective,
+ raw,
+ goToWalletManualWithdraw,
+ summary,
+ expiration: expiration
+ ? AbsoluteTime.fromProtocolTimestamp(expiration)
+ : undefined,
+ };
+
+ if (!foundBalance) {
+ return {
+ status: "no-balance-for-currency",
+ error: undefined,
+ balance: undefined,
+ ...baseResult,
+ payStatus: insufficientBalance,
+ };
+ }
+
+ const foundAmount = Amounts.parseOrThrow(foundBalance.available);
+
+ //FIXME: should use pay result type since it check for coins exceptions
+ if (Amounts.cmp(foundAmount, amount) < 0) {
+ //payStatus.status === PreparePayResultType.InsufficientBalance) {
+ return {
+ status: "no-enough-balance",
+ error: undefined,
+ balance: foundAmount,
+ ...baseResult,
+ payStatus: insufficientBalance,
+ };
+ }
+
+ async function accept(): Promise<void> {
+ const resp = await api.wallet.call(
+ WalletApiOperation.ConfirmPeerPullDebit,
+ {
+ transactionId,
+ },
+ );
+ onSuccess(resp.transactionId);
+ }
+
+ return {
+ status: "ready",
+ error: undefined,
+ ...baseResult,
+ payStatus: paymentPossible,
+ balance: foundAmount,
+ accept: {
+ onClick: pushAlertOnError(accept),
+ },
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx
new file mode 100644
index 000000000..8993476ea
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx
@@ -0,0 +1,56 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ AbsoluteTime,
+ PreparePayResult,
+ PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "invoice payment",
+};
+
+export const Ready = tests.createExample(ReadyView, {
+ effective: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ },
+ raw: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ },
+ summary: "some subject",
+ uri: "taler://pay/merchant.ar/123",
+ payStatus: {
+ status: PreparePayResultType.PaymentPossible,
+ amountEffective: "ARS:1",
+ } as PreparePayResult,
+ expiration: AbsoluteTime.fromMilliseconds(
+ new Date().getTime() + 1000 * 60 * 60,
+ ),
+ accept: {},
+ cancel: {},
+});
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/test.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/test.ts
new file mode 100644
index 000000000..4a3d08ed0
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/test.ts
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("Invoice payment state", () => {
+ it.skip("should create some states", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
new file mode 100644
index 000000000..547d5ac9a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Part } from "../../components/Part.js";
+import { PaymentButtons } from "../../components/PaymentButtons.js";
+import { Time } from "../../components/Time.js";
+import {
+ getAmountWithFee,
+ InvoicePaymentDetails,
+} from "../../wallet/Transaction.js";
+import { State } from "./index.js";
+
+export function ReadyView(
+ state: State.Ready | State.NoBalanceForCurrency | State.NoEnoughBalance,
+): VNode {
+ const { i18n } = useTranslationContext();
+ const { summary, effective, raw, expiration, uri, status, payStatus } = state;
+ return (
+ <Fragment>
+ <section style={{ textAlign: "left" }}>
+ <Part title={i18n.str`Subject`} text={<div>{summary}</div>} />
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <InvoicePaymentDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
+ />
+ }
+ />
+ <Part
+ title={i18n.str`Valid until`}
+ text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />}
+ kind="neutral"
+ />
+ </section>
+ <PaymentButtons
+ amount={effective}
+ payStatus={payStatus}
+ uri={uri}
+ payHandler={status === "ready" ? state.accept : undefined}
+ goToWalletManualWithdraw={state.goToWalletManualWithdraw}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx b/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx
deleted file mode 100644
index 622e7950f..000000000
--- a/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ContractTerms, PreparePayResultType } from '@gnu-taler/taler-util';
-import { createExample } from '../test-utils';
-import { PaymentRequestView as TestedComponent } from './Pay';
-
-export default {
- title: 'cta/pay',
- component: TestedComponent,
- argTypes: {
- },
-};
-
-export const NoBalance = createExample(TestedComponent, {
- payStatus: {
- status: PreparePayResultType.InsufficientBalance,
- noncePriv: '',
- proposalId: "proposal1234",
- contractTerms: {
- merchant: {
- name: 'someone'
- },
- summary: 'some beers',
- amount: 'USD:10',
- } as Partial<ContractTerms> as any,
- amountRaw: 'USD:10',
- }
-});
-
-export const NoEnoughBalance = createExample(TestedComponent, {
- payStatus: {
- status: PreparePayResultType.InsufficientBalance,
- noncePriv: '',
- proposalId: "proposal1234",
- contractTerms: {
- merchant: {
- name: 'someone'
- },
- summary: 'some beers',
- amount: 'USD:10',
- } as Partial<ContractTerms> as any,
- amountRaw: 'USD:10',
- },
- balance: {
- currency: 'USD',
- fraction: 40000000,
- value: 9
- }
-});
-
-export const PaymentPossible = createExample(TestedComponent, {
- uri: 'taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0',
- payStatus: {
- status: PreparePayResultType.PaymentPossible,
- amountEffective: 'USD:10',
- amountRaw: 'USD:10',
- noncePriv: '',
- contractTerms: {
- nonce: '123213123',
- merchant: {
- name: 'someone'
- },
- amount: 'USD:10',
- summary: 'some beers',
- } as Partial<ContractTerms> as any,
- contractTermsHash: '123456',
- proposalId: 'proposal1234'
- }
-});
-
-export const PaymentPossibleWithFee = createExample(TestedComponent, {
- uri: 'taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0',
- payStatus: {
- status: PreparePayResultType.PaymentPossible,
- amountEffective: 'USD:10.20',
- amountRaw: 'USD:10',
- noncePriv: '',
- contractTerms: {
- nonce: '123213123',
- merchant: {
- name: 'someone'
- },
- amount: 'USD:10',
- summary: 'some beers',
- } as Partial<ContractTerms> as any,
- contractTermsHash: '123456',
- proposalId: 'proposal1234'
- }
-});
-
-export const AlreadyConfirmedWithFullfilment = createExample(TestedComponent, {
- payStatus: {
- status: PreparePayResultType.AlreadyConfirmed,
- amountEffective: 'USD:10',
- amountRaw: 'USD:10',
- contractTerms: {
- merchant: {
- name: 'someone'
- },
- fulfillment_message: 'congratulations! you are looking at the fulfillment message! ',
- summary: 'some beers',
- amount: 'USD:10',
- } as Partial<ContractTerms> as any,
- contractTermsHash: '123456',
- proposalId: 'proposal1234',
- paid: false,
- }
-});
-
-export const AlreadyConfirmedWithoutFullfilment = createExample(TestedComponent, {
- payStatus: {
- status: PreparePayResultType.AlreadyConfirmed,
- amountEffective: 'USD:10',
- amountRaw: 'USD:10',
- contractTerms: {
- merchant: {
- name: 'someone'
- },
- summary: 'some beers',
- amount: 'USD:10',
- } as Partial<ContractTerms> as any,
- contractTermsHash: '123456',
- proposalId: 'proposal1234',
- paid: false,
- }
-});
-
-export const AlreadyPaid = createExample(TestedComponent, {
- payStatus: {
- status: PreparePayResultType.AlreadyConfirmed,
- amountEffective: 'USD:10',
- amountRaw: 'USD:10',
- contractTerms: {
- merchant: {
- name: 'someone'
- },
- fulfillment_message: 'congratulations! you are looking at the fulfillment message! ',
- summary: 'some beers',
- amount: 'USD:10',
- } as Partial<ContractTerms> as any,
- contractTermsHash: '123456',
- proposalId: 'proposal1234',
- paid: true,
- }
-});
diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx b/packages/taler-wallet-webextension/src/cta/Pay.tsx
deleted file mode 100644
index 675b14ff9..000000000
--- a/packages/taler-wallet-webextension/src/cta/Pay.tsx
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- This file is part of TALER
- (C) 2015 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Page shown to the user to confirm entering
- * a contract.
- */
-
-/**
- * Imports.
- */
-// import * as i18n from "../i18n";
-
-import { AmountJson, AmountLike, Amounts, ConfirmPayResult, ConfirmPayResultDone, ConfirmPayResultType, ContractTerms, getJsonI18n, i18n, PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util";
-import { Fragment, JSX, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { LogoHeader } from "../components/LogoHeader";
-import { Part } from "../components/Part";
-import { QR } from "../components/QR";
-import { ButtonSuccess, ErrorBox, LinkSuccess, SuccessBox, WalletAction, WarningBox } from "../components/styled";
-import { useBalances } from "../hooks/useBalances";
-import * as wxApi from "../wxApi";
-
-interface Props {
- talerPayUri?: string
-}
-
-// export function AlreadyPaid({ payStatus }: { payStatus: PreparePayResult }) {
-// const fulfillmentUrl = payStatus.contractTerms.fulfillment_url;
-// let message;
-// if (fulfillmentUrl) {
-// message = (
-// <span>
-// You have already paid for this article. Click{" "}
-// <a href={fulfillmentUrl} target="_bank" rel="external">here</a> to view it again.
-// </span>
-// );
-// } else {
-// message = <span>
-// You have already paid for this article:{" "}
-// <em>
-// {payStatus.contractTerms.fulfillment_message ?? "no message given"}
-// </em>
-// </span>;
-// }
-// return <section class="main">
-// <h1>GNU Taler Wallet</h1>
-// <article class="fade">
-// {message}
-// </article>
-// </section>
-// }
-
-const doPayment = async (payStatus: PreparePayResult): Promise<ConfirmPayResultDone> => {
- if (payStatus.status !== "payment-possible") {
- throw Error(`invalid state: ${payStatus.status}`);
- }
- const proposalId = payStatus.proposalId;
- const res = await wxApi.confirmPay(proposalId, undefined);
- if (res.type !== ConfirmPayResultType.Done) {
- throw Error("payment pending");
- }
- const fu = res.contractTerms.fulfillment_url;
- if (fu) {
- document.location.href = fu;
- }
- return res;
-};
-
-
-
-export function PayPage({ talerPayUri }: Props): JSX.Element {
- const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(undefined);
- const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(undefined);
- const [payErrMsg, setPayErrMsg] = useState<string | undefined>(undefined);
-
- const balance = useBalances()
- const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || [])
-
- const foundBalance = balanceWithoutError.find(b => payStatus && Amounts.parseOrThrow(b.available).currency === Amounts.parseOrThrow(payStatus?.amountRaw).currency)
- const foundAmount = foundBalance ? Amounts.parseOrThrow(foundBalance.available) : undefined
-
- useEffect(() => {
- if (!talerPayUri) return;
- const doFetch = async (): Promise<void> => {
- try {
- const p = await wxApi.preparePay(talerPayUri);
- setPayStatus(p);
- } catch (e) {
- if (e instanceof Error) {
- setPayErrMsg(e.message)
- }
- }
- };
- doFetch();
- }, [talerPayUri]);
-
- if (!talerPayUri) {
- return <span>missing pay uri</span>
- }
-
- if (!payStatus) {
- if (payErrMsg) {
- return <WalletAction>
- <LogoHeader />
- <h2>
- {i18n.str`Digital cash payment`}
- </h2>
- <section>
- <p>Could not get the payment information for this order</p>
- <ErrorBox>
- {payErrMsg}
- </ErrorBox>
- </section>
- </WalletAction>
- }
- return <span>Loading payment information ...</span>;
- }
-
- const onClick = async () => {
- try {
- const res = await doPayment(payStatus)
- setPayResult(res);
- } catch (e) {
- console.error(e);
- if (e instanceof Error) {
- setPayErrMsg(e.message);
- }
- }
-
- }
-
- return <PaymentRequestView uri={talerPayUri}
- payStatus={payStatus} payResult={payResult}
- onClick={onClick} payErrMsg={payErrMsg}
- balance={foundAmount} />;
-}
-
-export interface PaymentRequestViewProps {
- payStatus: PreparePayResult;
- payResult?: ConfirmPayResult;
- onClick: () => void;
- payErrMsg?: string;
- uri: string;
- balance: AmountJson | undefined;
-}
-export function PaymentRequestView({ uri, payStatus, payResult, onClick, payErrMsg, balance }: PaymentRequestViewProps) {
- let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw);
- const contractTerms: ContractTerms = payStatus.contractTerms;
-
- if (!contractTerms) {
- return (
- <span>
- Error: did not get contract terms from merchant or wallet backend.
- </span>
- );
- }
-
- if (payStatus.status === PreparePayResultType.PaymentPossible) {
- const amountRaw = Amounts.parseOrThrow(payStatus.amountRaw);
- const amountEffective: AmountJson = Amounts.parseOrThrow(
- payStatus.amountEffective,
- );
- totalFees = Amounts.sub(amountEffective, amountRaw).amount;
- }
-
- let merchantName: VNode;
- if (contractTerms.merchant && contractTerms.merchant.name) {
- merchantName = <strong>{contractTerms.merchant.name}</strong>;
- } else {
- merchantName = <strong>(pub: {contractTerms.merchant_pub})</strong>;
- }
-
- function Alternative() {
- const [showQR, setShowQR] = useState<boolean>(false)
- const privateUri = payStatus.status !== PreparePayResultType.AlreadyConfirmed ? `${uri}&n=${payStatus.noncePriv}` : uri
- return <section>
- <LinkSuccess upperCased onClick={() => setShowQR(qr => !qr)}>
- {!showQR ? i18n.str`Pay with a mobile phone` : i18n.str`Hide QR`}
- </LinkSuccess>
- {showQR && <div>
- <QR text={privateUri} />
- Scan the QR code or <a href={privateUri}>click here</a>
- </div>}
- </section>
- }
-
- function ButtonsSection() {
- if (payResult) {
- if (payResult.type === ConfirmPayResultType.Pending) {
- return <section>
- <div>
- <p>Processing...</p>
- </div>
- </section>
- }
- return null
- }
- if (payErrMsg) {
- return <section>
- <div>
- <p>Payment failed: {payErrMsg}</p>
- <button class="pure-button button-success" onClick={onClick} >
- {i18n.str`Retry`}
- </button>
- </div>
- </section>
- }
- if (payStatus.status === PreparePayResultType.PaymentPossible) {
- return <Fragment>
- <section>
- <ButtonSuccess upperCased onClick={onClick}>
- {i18n.str`Pay`} {amountToString(payStatus.amountEffective)}
- </ButtonSuccess>
- </section>
- <Alternative />
- </Fragment>
- }
- if (payStatus.status === PreparePayResultType.InsufficientBalance) {
- return <Fragment>
- <section>
- {balance ? <WarningBox>
- Your balance of {amountToString(balance)} is not enough to pay for this purchase
- </WarningBox> : <WarningBox>
- Your balance is not enough to pay for this purchase.
- </WarningBox>}
- </section>
- <section>
- <ButtonSuccess upperCased>
- {i18n.str`Withdraw digital cash`}
- </ButtonSuccess>
- </section>
- <Alternative />
- </Fragment>
- }
- if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
- return <Fragment>
- <section>
- {payStatus.paid && contractTerms.fulfillment_message && <Part title="Merchant message" text={contractTerms.fulfillment_message} kind='neutral' />}
- </section>
- {!payStatus.paid && <Alternative />}
- </Fragment>
- }
- return <span />
- }
-
- return <WalletAction>
- <LogoHeader />
-
- <h2>
- {i18n.str`Digital cash payment`}
- </h2>
- {payStatus.status === PreparePayResultType.AlreadyConfirmed &&
- (payStatus.paid ? <SuccessBox> Already paid </SuccessBox> : <WarningBox> Already claimed </WarningBox>)
- }
- {payResult && payResult.type === ConfirmPayResultType.Done && (
- <SuccessBox>
- <h3>Payment complete</h3>
- <p>{!payResult.contractTerms.fulfillment_message ?
- "You will now be sent back to the merchant you came from." :
- payResult.contractTerms.fulfillment_message
- }</p>
- </SuccessBox>
- )}
- <section>
- {payStatus.status !== PreparePayResultType.InsufficientBalance && Amounts.isNonZero(totalFees) &&
- <Part big title="Total to pay" text={amountToString(payStatus.amountEffective)} kind='negative' />
- }
- <Part big title="Purchase amount" text={amountToString(payStatus.amountRaw)} kind='neutral' />
- {Amounts.isNonZero(totalFees) && <Fragment>
- <Part big title="Fee" text={amountToString(totalFees)} kind='negative' />
- </Fragment>
- }
- <Part title="Merchant" text={contractTerms.merchant.name} kind='neutral' />
- <Part title="Purchase" text={contractTerms.summary} kind='neutral' />
- {contractTerms.order_id && <Part title="Receipt" text={`#${contractTerms.order_id}`} kind='neutral' />}
- </section>
- <ButtonsSection />
-
- </WalletAction>
-}
-
-function amountToString(text: AmountLike) {
- const aj = Amounts.jsonifyAmount(text)
- const amount = Amounts.stringifyValue(aj, 2)
- return `${amount} ${aj.currency}`
-}
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/index.ts b/packages/taler-wallet-webextension/src/cta/Payment/index.ts
new file mode 100644
index 000000000..c9bead89c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Payment/index.ts
@@ -0,0 +1,101 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AmountJson,
+ PreparePayResult,
+ PreparePayResultAlreadyConfirmed,
+ PreparePayResultInsufficientBalance,
+ PreparePayResultPaymentPossible,
+} from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { BaseView } from "./views.js";
+
+export interface Props {
+ talerPayUri: string;
+ goToWalletManualWithdraw: (amount?: string) => Promise<void>;
+ cancel: () => Promise<void>;
+ onSuccess: (tx: string) => Promise<void>;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | State.Ready
+ | State.NoEnoughBalance
+ | State.NoBalanceForCurrency
+ | State.Confirmed;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ interface BaseInfo {
+ amount: AmountJson;
+ uri: string;
+ error: undefined;
+ goToWalletManualWithdraw: (amount?: string) => Promise<void>;
+ cancel: () => Promise<void>;
+ }
+ export interface NoBalanceForCurrency extends BaseInfo {
+ status: "no-balance-for-currency";
+ payStatus: PreparePayResult;
+ balance: undefined;
+ }
+ export interface NoEnoughBalance extends BaseInfo {
+ status: "no-enough-balance";
+ payStatus: PreparePayResultInsufficientBalance;
+ balance: AmountJson;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ payStatus: PreparePayResultPaymentPossible;
+ payHandler: ButtonHandler;
+ balance: AmountJson;
+ }
+
+ export interface Confirmed extends BaseInfo {
+ status: "confirmed";
+ payStatus: PreparePayResultAlreadyConfirmed;
+ balance: AmountJson;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ "no-balance-for-currency": BaseView,
+ "no-enough-balance": BaseView,
+ confirmed: BaseView,
+ ready: BaseView,
+};
+
+export const PaymentPage = compose(
+ "Payment",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/state.ts b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
new file mode 100644
index 000000000..4733e5aee
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
@@ -0,0 +1,174 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Amounts,
+ ConfirmPayResultType,
+ NotificationType,
+ PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useEffect } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerPayUri,
+ cancel,
+ goToWalletManualWithdraw,
+ onSuccess,
+}: Props): State {
+ const { pushAlertOnError } = useAlertContext();
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+
+ const hook = useAsyncAsHook(async () => {
+ if (!talerPayUri) throw Error("ERROR_NO-URI-FOR-PAYMENT");
+ const payStatus = await api.wallet.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: talerPayUri,
+ },
+ );
+ const balance = await api.wallet.call(WalletApiOperation.GetBalances, {});
+ return { payStatus, balance, uri: talerPayUri };
+ }, []);
+
+ useEffect(
+ () =>
+ api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
+ hook?.retry,
+ ),
+ [hook],
+ );
+
+ const hookResponse = !hook || hook.hasError ? undefined : hook.response;
+
+ useEffect(() => {
+ if (!hookResponse) return;
+ const { payStatus } = hookResponse;
+ if (
+ payStatus &&
+ payStatus.status === PreparePayResultType.AlreadyConfirmed &&
+ payStatus.paid
+ ) {
+ const fu = payStatus.contractTerms.fulfillment_url;
+ if (fu) {
+ setTimeout(() => {
+ document.location.href = fu;
+ }, 3000);
+ }
+ }
+ }, [hookResponse]);
+
+ if (!hook) return { status: "loading", error: undefined };
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the payment and balance status`,
+ hook,
+ ),
+ };
+ }
+ // if (hook.hasError) {
+ // return {
+ // status: "loading-uri",
+ // error: hook,
+ // };
+ // }
+ const { payStatus } = hook.response;
+
+ const amount = Amounts.parseOrThrow(payStatus.amountRaw);
+
+ const foundBalance = hook.response.balance.balances.find(
+ (b) => Amounts.parseOrThrow(b.available).currency === amount.currency,
+ );
+
+ const baseResult = {
+ uri: hook.response.uri,
+ amount,
+ error: undefined,
+ cancel,
+ goToWalletManualWithdraw,
+ };
+
+ if (!foundBalance) {
+ return {
+ status: "no-balance-for-currency",
+ balance: undefined,
+ payStatus,
+ ...baseResult,
+ };
+ }
+
+ const foundAmount = Amounts.parseOrThrow(foundBalance.available);
+
+ if (payStatus.status === PreparePayResultType.InsufficientBalance) {
+ return {
+ status: "no-enough-balance",
+ balance: foundAmount,
+ payStatus,
+ ...baseResult,
+ };
+ }
+
+ if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
+ return {
+ status: "confirmed",
+ balance: foundAmount,
+ payStatus,
+ ...baseResult,
+ };
+ }
+
+ async function doPayment(): Promise<void> {
+ const res = await api.wallet.call(WalletApiOperation.ConfirmPay, {
+ proposalId: payStatus.proposalId,
+ });
+ // handle confirm pay
+ if (res.type !== ConfirmPayResultType.Done) {
+ onSuccess(res.transactionId);
+ return;
+ }
+ const fu = res.contractTerms.fulfillment_url;
+ if (fu) {
+ if (typeof window !== "undefined") {
+ document.location.href = fu;
+ }
+ }
+ onSuccess(res.transactionId);
+ }
+
+ const payHandler: ButtonHandler = {
+ onClick: pushAlertOnError(doPayment),
+ };
+
+ // (payStatus.status === PreparePayResultType.PaymentPossible)
+ return {
+ status: "ready",
+ payHandler,
+ payStatus,
+ ...baseResult,
+ balance: foundAmount,
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
new file mode 100644
index 000000000..d03f48746
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
@@ -0,0 +1,513 @@
+/*
+ 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 {
+ AmountString,
+ Amounts,
+ MerchantContractTerms as ContractTerms,
+ PreparePayResultType,
+ TransactionIdStr,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import beer from "../../../static-dev/beer.png";
+import merchantIcon from "../../../static-dev/merchant-icon.jpeg";
+import { nullFunction } from "../../mui/handlers.js";
+import { BaseView } from "./views.js";
+
+export default {
+ title: "payment",
+ component: BaseView,
+ argTypes: {},
+};
+
+export const NoEnoughBalanceAvailable = tests.createExample(BaseView, {
+ status: "no-enough-balance",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 12,
+ },
+
+ uri: "",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:9" as AmountString,
+ balanceMaterial: "USD:9" as AmountString,
+ balanceAgeAcceptable: "USD:9" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
+ talerUri: "taler://pay/..",
+
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ contractTerms: {
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ summary: "some beers",
+ amount: "USD:10",
+ } as Partial<ContractTerms> as any,
+ amountRaw: "USD:10" as AmountString,
+ },
+});
+
+export const NoEnoughBalanceMaterial = tests.createExample(BaseView, {
+ status: "no-enough-balance",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 12,
+ },
+
+ uri: "",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:10" as AmountString,
+ balanceMaterial: "USD:9" as AmountString,
+ balanceAgeAcceptable: "USD:9" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:0" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
+ talerUri: "taler://pay/..",
+
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ contractTerms: {
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ summary: "some beers",
+ amount: "USD:10" as AmountString,
+ } as Partial<ContractTerms> as any,
+ amountRaw: "USD:10" as AmountString,
+ },
+});
+
+export const NoEnoughBalanceAgeAcceptable = tests.createExample(BaseView, {
+ status: "no-enough-balance",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 12,
+ },
+
+ uri: "",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:10" as AmountString,
+ balanceMaterial: "USD:10" as AmountString,
+ balanceAgeAcceptable: "USD:9" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
+ talerUri: "taler://pay/..",
+
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ contractTerms: {
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ minimum_age: 18,
+ summary: "some beers",
+ amount: "USD:10",
+ } as Partial<ContractTerms> as any,
+ amountRaw: "USD:10" as AmountString,
+ },
+});
+
+export const NoEnoughBalanceMerchantAcceptable = tests.createExample(BaseView, {
+ status: "no-enough-balance",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 12,
+ },
+
+ uri: "",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:10" as AmountString,
+ balanceMaterial: "USD:10" as AmountString,
+ balanceAgeAcceptable: "USD:10" as AmountString,
+ balanceReceiverAcceptable: "USD:9" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
+ talerUri: "taler://pay/..",
+
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ contractTerms: {
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ summary: "some beers",
+ amount: "USD:10" as AmountString,
+ } as Partial<ContractTerms> as any,
+ amountRaw: "USD:10" as AmountString,
+ },
+});
+
+export const NoEnoughBalanceMerchantDepositable = tests.createExample(
+ BaseView,
+ {
+ status: "no-enough-balance",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 12,
+ },
+
+ uri: "",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:10" as AmountString,
+ balanceMaterial: "USD:10" as AmountString,
+ balanceAgeAcceptable: "USD:10" as AmountString,
+ balanceReceiverAcceptable: "USD:10" as AmountString,
+ balanceReceiverDepositable: "USD:9" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
+ talerUri: "taler://pay/..",
+
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ contractTerms: {
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ summary: "some beers",
+ amount: "USD:10" as AmountString,
+ } as Partial<ContractTerms> as any,
+ amountRaw: "USD:10" as AmountString,
+ },
+ },
+);
+
+export const NoEnoughBalanceFeeGap = tests.createExample(BaseView, {
+ status: "no-enough-balance",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 12,
+ },
+
+ uri: "",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.InsufficientBalance,
+ balanceDetails: {
+ amountRequested: "USD:10" as AmountString,
+ balanceAvailable: "USD:10" as AmountString,
+ balanceMaterial: "USD:10" as AmountString,
+ balanceAgeAcceptable: "USD:10" as AmountString,
+ balanceReceiverAcceptable: "USD:10" as AmountString,
+ balanceReceiverDepositable: "USD:10" as AmountString,
+ maxEffectiveSpendAmount: "USD:9.5" as AmountString,
+ balanceExchangeDepositable: "USD:9.5" as AmountString,
+ perExchange: {},
+ },
+ talerUri: "taler://pay/..",
+
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ contractTerms: {
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ minimum_age: 18,
+ summary: "some beers",
+ amount: "USD:10" as AmountString,
+ } as Partial<ContractTerms> as any,
+ amountRaw: "USD:10" as AmountString,
+ },
+});
+
+export const PaymentPossible = tests.createExample(BaseView, {
+ status: "ready",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 11,
+ },
+ payHandler: {
+ onClick: nullFunction,
+ },
+
+ uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.PaymentPossible,
+ talerUri: "taler://pay/..",
+ amountEffective: "USD:10" as AmountString,
+ amountRaw: "USD:10" as AmountString,
+
+ contractTerms: {
+ nonce: "123213123",
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ pay_deadline: {
+ t_s: new Date().getTime() / 1000 + 60 * 60 * 3,
+ },
+ amount: "USD:10" as AmountString,
+ summary: "some beers",
+ } as Partial<ContractTerms> as any,
+ contractTermsHash: "123456",
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ },
+});
+
+export const PaymentPossibleWithFee = tests.createExample(BaseView, {
+ status: "ready",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 11,
+ },
+ payHandler: {
+ onClick: nullFunction,
+ },
+
+ uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.PaymentPossible,
+ talerUri: "taler://pay/..",
+ amountEffective: "USD:10.20" as AmountString,
+ amountRaw: "USD:10" as AmountString,
+
+ contractTerms: {
+ nonce: "123213123",
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ amount: "USD:10" as AmountString,
+ summary: "some beers",
+ } as Partial<ContractTerms> as any,
+ contractTermsHash: "123456",
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ },
+});
+
+export const TicketWithAProductList = tests.createExample(BaseView, {
+ status: "ready",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 11,
+ },
+ payHandler: {
+ onClick: nullFunction,
+ },
+
+ uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.PaymentPossible,
+ talerUri: "taler://pay/..",
+ amountEffective: "USD:10.20" as AmountString,
+ amountRaw: "USD:10" as AmountString,
+
+ contractTerms: {
+ nonce: "123213123",
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ amount: "USD:10",
+ summary: "some beers",
+ products: [
+ {
+ description: "ten beers",
+ price: "USD:1",
+ quantity: 10,
+ image: beer,
+ },
+ {
+ description: "beer without image",
+ price: "USD:1",
+ quantity: 10,
+ },
+ {
+ description: "one brown beer",
+ price: "USD:2",
+ quantity: 1,
+ image: beer,
+ },
+ ],
+ } as Partial<ContractTerms> as any,
+ contractTermsHash: "123456",
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ },
+});
+
+export const TicketWithShipping = tests.createExample(BaseView, {
+ status: "ready",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 11,
+ },
+ payHandler: {
+ onClick: nullFunction,
+ },
+
+ uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.PaymentPossible,
+ talerUri: "taler://pay/..",
+ amountEffective: "USD:10.20" as AmountString,
+ amountRaw: "USD:10" as AmountString,
+
+ contractTerms: {
+ nonce: "123213123",
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ amount: "USD:10",
+ summary: "banana pi set",
+ products: [
+ {
+ description: "banana pi",
+ price: "USD:2",
+ quantity: 1,
+ },
+ ],
+ delivery_date: {
+ t_s: new Date().getTime() / 1000 + 30 * 24 * 60 * 60,
+ },
+ delivery_location: {
+ town: "Liverpool",
+ street: "Down st 1234",
+ },
+ } as Partial<ContractTerms> as any,
+ contractTermsHash: "123456",
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ },
+});
+
+export const AlreadyConfirmedByOther = tests.createExample(BaseView, {
+ status: "confirmed",
+ error: undefined,
+ amount: Amounts.parseOrThrow("USD:10"),
+ balance: {
+ currency: "USD",
+ fraction: 40000000,
+ value: 11,
+ },
+
+ uri: "taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
+ payStatus: {
+ transactionId: " " as TransactionIdStr,
+ status: PreparePayResultType.AlreadyConfirmed,
+ talerUri: "taler://pay/..",
+ amountEffective: "USD:10" as AmountString,
+ amountRaw: "USD:10" as AmountString,
+ contractTerms: {
+ merchant: {
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
+ },
+ summary: "some beers",
+ amount: "USD:10",
+ } as Partial<ContractTerms> as any,
+ contractTermsHash: "123456",
+ proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+ paid: false,
+ },
+});
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/test.ts b/packages/taler-wallet-webextension/src/cta/Payment/test.ts
new file mode 100644
index 000000000..5847cc833
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Payment/test.ts
@@ -0,0 +1,576 @@
+/*
+ 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 {
+ AmountString,
+ Amounts,
+ ConfirmPayResult,
+ ConfirmPayResultType,
+ NotificationType,
+ PreparePayResultInsufficientBalance,
+ PreparePayResultPaymentPossible,
+ PreparePayResultType,
+ ScopeType,
+ TransactionMajorState,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { expect } from "chai";
+import * as tests from "@gnu-taler/web-util/testing";
+import { ErrorAlert, useAlertContext } from "../../context/alert.js";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
+import { useComponentState } from "./state.js";
+
+describe("Payment CTA states", () => {
+ it("should tell the user that the URI is missing", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerPayUri: "",
+ cancel: nullFunction,
+ goToWalletManualWithdraw: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ ({ status, error }) => {
+ expect(status).equals("error");
+ if (error === undefined) expect.fail();
+ // expect(error.hasError).true;
+ // expect(error.operational).false;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should response with no balance", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerPayUri: "taller://pay",
+ cancel: nullFunction,
+ goToWalletManualWithdraw: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PreparePayForUri,
+ undefined,
+ {
+ status: PreparePayResultType.InsufficientBalance,
+ amountRaw: "USD:10",
+ } as PreparePayResultInsufficientBalance,
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetBalances,
+ {},
+ { balances: [] },
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "no-balance-for-currency") {
+ expect(state).eq({});
+ return;
+ }
+ expect(state.balance).undefined;
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should not be able to pay if there is no enough balance", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerPayUri: "taller://pay",
+ cancel: nullFunction,
+ goToWalletManualWithdraw: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PreparePayForUri,
+ undefined,
+ {
+ status: PreparePayResultType.InsufficientBalance,
+ amountRaw: "USD:10",
+ } as PreparePayResultInsufficientBalance,
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetBalances,
+ {},
+ {
+ balances: [
+ {
+ flags: [],
+ available: "USD:5" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ },
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "no-enough-balance") expect.fail();
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:5"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should be able to pay (without fee)", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerPayUri: "taller://pay",
+ cancel: nullFunction,
+ goToWalletManualWithdraw: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PreparePayForUri,
+ undefined,
+ {
+ status: PreparePayResultType.PaymentPossible,
+ amountRaw: "USD:10",
+ amountEffective: "USD:10",
+ } as PreparePayResultPaymentPossible,
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetBalances,
+ {},
+ {
+ balances: [
+ {
+ flags: [],
+ available: "USD:15" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ },
+ );
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") {
+ expect(state).eq({});
+ return;
+ }
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
+ expect(state.payHandler.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should be able to pay (with fee)", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerPayUri: "taller://pay",
+ cancel: nullFunction,
+ goToWalletManualWithdraw: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PreparePayForUri,
+ undefined,
+ {
+ status: PreparePayResultType.PaymentPossible,
+ amountRaw: "USD:9",
+ amountEffective: "USD:10",
+ } as PreparePayResultPaymentPossible,
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetBalances,
+ {},
+ {
+ balances: [
+ {
+ flags: [],
+ available: "USD:15" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ },
+ );
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+ expect(state.payHandler.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should get confirmation done after pay successfully", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerPayUri: "taller://pay",
+ cancel: nullFunction,
+ goToWalletManualWithdraw: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PreparePayForUri,
+ undefined,
+ {
+ status: PreparePayResultType.PaymentPossible,
+ amountRaw: "USD:9",
+ amountEffective: "USD:10",
+ } as PreparePayResultPaymentPossible,
+ );
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetBalances,
+ {},
+ {
+ balances: [
+ {
+ flags: [],
+ available: "USD:15" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ },
+ );
+ handler.addWalletCallResponse(WalletApiOperation.ConfirmPay, undefined, {
+ type: ConfirmPayResultType.Done,
+ contractTerms: {},
+ } as ConfirmPayResult);
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") {
+ expect(state).eq({});
+ return;
+ }
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+ if (state.payHandler.onClick === undefined) expect.fail();
+ state.payHandler.onClick();
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should not stay in ready state after pay with error", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerPayUri: "taller://pay",
+ cancel: nullFunction,
+ goToWalletManualWithdraw: nullFunction,
+ onSuccess: nullFunction,
+ };
+ handler.addWalletCallResponse(
+ WalletApiOperation.PreparePayForUri,
+ undefined,
+ {
+ status: PreparePayResultType.PaymentPossible,
+ amountRaw: "USD:9",
+ amountEffective: "USD:10",
+ } as PreparePayResultPaymentPossible,
+ );
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetBalances,
+ {},
+ {
+ balances: [
+ {
+ flags: [],
+ available: "USD:15" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ },
+ );
+ handler.addWalletCallResponse(WalletApiOperation.ConfirmPay, undefined, {
+ type: ConfirmPayResultType.Pending,
+ lastError: { code: 1 },
+ } as ConfirmPayResult);
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ () => {
+ const state = useComponentState(props);
+ // const { alerts } = useAlertContext();
+ return { ...state, alerts: {} };
+ },
+ {},
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+ // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+ if (state.payHandler.onClick === undefined) expect.fail();
+ state.payHandler.onClick();
+ },
+ // (state) => {
+ // if (state.status !== "ready") expect.fail();
+ // expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ // expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+
+ // // FIXME: check that the error is pushed to the alertContext
+ // // expect(state.alerts.length).eq(1);
+ // // const alert = state.alerts[0]
+ // // if (alert.type !== "error") expect.fail();
+
+ // // expect(alert.cause.errorDetail.payResult).deep.equal({
+ // // type: ConfirmPayResultType.Pending,
+ // // lastError: { code: 1 },
+ // // });
+ // },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should update balance if a coins is withdraw", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ const props = {
+ talerPayUri: "taller://pay",
+ cancel: nullFunction,
+ goToWalletManualWithdraw: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PreparePayForUri,
+ undefined,
+ {
+ status: PreparePayResultType.PaymentPossible,
+ amountRaw: "USD:9",
+ amountEffective: "USD:10",
+ } as PreparePayResultPaymentPossible,
+ );
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetBalances,
+ {},
+ {
+ balances: [
+ {
+ flags: [],
+ available: "USD:10" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ },
+ );
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PreparePayForUri,
+ undefined,
+ {
+ status: PreparePayResultType.PaymentPossible,
+ amountRaw: "USD:9",
+ amountEffective: "USD:10",
+ } as PreparePayResultPaymentPossible,
+ );
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetBalances,
+ {},
+ {
+ balances: [
+ {
+ flags: [],
+ available: "USD:15" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "USD",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ },
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+ // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+ expect(state.payHandler.onClick).not.undefined;
+
+ handler.notifyEventFromWallet({
+ type: NotificationType.TransactionStateTransition,
+ newTxState: {} as any,
+ oldTxState: {} as any,
+ transactionId: "123",
+ }
+
+ );
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
+ expect(state.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
+ // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+ expect(state.payHandler.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
new file mode 100644
index 000000000..68d161ab2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.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 {
+ AbsoluteTime,
+ Amounts,
+ MerchantContractTerms as ContractTerms,
+ PreparePayResultType,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+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 { ShowFullContractTermPopup } from "../../components/ShowFullContractTermPopup.js";
+import { Time } from "../../components/Time.js";
+import {
+ AgeSign,
+ 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
+ | State.Confirmed
+ | State.NoBalanceForCurrency
+ | State.NoEnoughBalance;
+
+export function BaseView(state: SupportedStates): VNode {
+ const { i18n } = useTranslationContext();
+
+ const contractTerms: ContractTerms = state.payStatus.contractTerms;
+
+ const effective =
+ "amountEffective" in state.payStatus
+ ? state.payStatus.amountEffective
+ ? Amounts.parseOrThrow(state.payStatus.amountEffective)
+ : Amounts.zeroOfCurrency(state.amount.currency)
+ : state.amount;
+
+ return (
+ <Fragment>
+ <ShowImportantMessage state={state} />
+
+ <section style={{ textAlign: "left" }}>
+ <Part
+ title={
+ contractTerms.minimum_age ? (
+ <Fragment>
+ <i18n.Translate>Purchase</i18n.Translate>
+ &nbsp;
+ <AgeSign size={20} title={i18n.str`This purchase is age restricted.`}>{contractTerms.minimum_age}+</AgeSign>
+ </Fragment>
+ ) : (
+ <i18n.Translate>Purchase</i18n.Translate>
+ )
+ }
+ text={contractTerms.summary as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Merchant`}
+ text={<MerchantDetails merchant={contractTerms.merchant} />}
+ kind="neutral"
+ />
+ {contractTerms.pay_deadline && (
+ <Part
+ title={i18n.str`Valid until`}
+ text={
+ <Time
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ contractTerms.pay_deadline,
+ )}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ }
+ kind="neutral"
+ />
+ )}
+ </section>
+ <EnabledBySettings name="advancedMode">
+ <section style={{ textAlign: "left" }}>
+ <ShowFullContractTermPopup
+ transactionId={state.payStatus.transactionId}
+ />
+ </section>
+ </EnabledBySettings>
+ <PaymentButtons
+ amount={effective}
+ payStatus={state.payStatus}
+ uri={state.uri}
+ payHandler={state.status === "ready" ? state.payHandler : undefined}
+ goToWalletManualWithdraw={state.goToWalletManualWithdraw}
+ />
+ </Fragment>
+ );
+}
+
+function ShowImportantMessage({ state }: { state: SupportedStates }): VNode {
+ const { i18n } = useTranslationContext();
+ const { payStatus } = state;
+ if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
+ if (payStatus.paid) {
+ if (payStatus.contractTerms.fulfillment_url) {
+ return (
+ <SuccessBox>
+ <i18n.Translate>
+ Already paid, you are going to be redirected to{" "}
+ <a href={payStatus.contractTerms.fulfillment_url}>
+ {payStatus.contractTerms.fulfillment_url}
+ </a>
+ </i18n.Translate>
+ </SuccessBox>
+ );
+ }
+ return (
+ <SuccessBox>
+ <i18n.Translate>Already paid</i18n.Translate>
+ </SuccessBox>
+ );
+ }
+ return (
+ <WarningBox>
+ <i18n.Translate>Already claimed</i18n.Translate>
+ </WarningBox>
+ );
+ }
+
+ return <Fragment />;
+}
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts
new file mode 100644
index 000000000..1e903fe46
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.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 { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+import { PaymentPage } from "../Payment/index.js";
+import {
+ AmountFieldHandler,
+ ButtonHandler,
+ TextFieldHandler,
+} from "../../mui/handlers.js";
+
+export interface Props {
+ talerTemplateUri: string;
+ goToWalletManualWithdraw: (amount?: string) => Promise<void>;
+ cancel: () => Promise<void>;
+ onSuccess: (tx: string) => Promise<void>;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | State.OrderReady
+ | State.FillTemplate;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface FillTemplate {
+ status: "fill-template";
+ error: undefined;
+ amount?: AmountFieldHandler;
+ summary?: TextFieldHandler;
+ minAge: number;
+ onCreate: ButtonHandler;
+ }
+
+ export interface OrderReady {
+ status: "order-ready";
+ error: undefined;
+ talerPayUri: string;
+ onSuccess: (tx: string) => Promise<void>;
+ cancel: () => Promise<void>;
+ goToWalletManualWithdraw: () => Promise<void>;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ "fill-template": ReadyView,
+ "order-ready": PaymentPage,
+};
+
+export const PaymentTemplatePage = compose(
+ "PaymentTemplate",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
new file mode 100644
index 000000000..20edb98eb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
@@ -0,0 +1,186 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts, PreparePayResult } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { AmountFieldHandler, TextFieldHandler } from "../../mui/handlers.js";
+import { RecursiveState } from "../../utils/index.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerTemplateUri,
+ cancel,
+ goToWalletManualWithdraw,
+ onSuccess,
+}: Props): RecursiveState<State> {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const { safely } = useAlertContext();
+
+ // const url = talerTemplateUri ? new URL(talerTemplateUri) : undefined;
+ // const parsedAmount = !amountParam ? undefined : Amounts.parse(amountParam);
+ // const currency = parsedAmount ? parsedAmount.currency : amountParam;
+
+ // const initialAmount =
+ // parsedAmount ?? (currency ? Amounts.zeroOfCurrency(currency) : undefined);
+
+ const [newOrder, setNewOrder] = useState("");
+
+ const hook = useAsyncAsHook(async () => {
+ if (!talerTemplateUri) throw Error("ERROR_NO-URI-FOR-PAYMENT-TEMPLATE");
+ const templateP = await api.wallet.call(
+ WalletApiOperation.CheckPayForTemplate, { talerPayTemplateUri: talerTemplateUri },
+ );
+ const requireMoreInfo = !templateP.template_contract.amount || !templateP.template_contract.summary;
+ let payStatus: PreparePayResult | undefined = undefined;
+ if (!requireMoreInfo) {
+ payStatus = await api.wallet.call(WalletApiOperation.PreparePayForTemplate, { talerPayTemplateUri: talerTemplateUri });
+ }
+ const balance = await api.wallet.call(WalletApiOperation.GetBalances, {});
+ return { payStatus, balance, uri: talerTemplateUri, templateP };
+ }, []);
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the status of the order template`,
+ hook,
+ ),
+ };
+ }
+
+ if (hook.response.payStatus) {
+ return {
+ status: "order-ready",
+ error: undefined,
+ cancel,
+ goToWalletManualWithdraw,
+ onSuccess,
+ talerPayUri: hook.response.payStatus.talerUri!,
+ };
+ }
+
+ if (newOrder) {
+ return {
+ status: "order-ready",
+ error: undefined,
+ cancel,
+ goToWalletManualWithdraw,
+ onSuccess,
+ talerPayUri: newOrder,
+ };
+ }
+
+ return () => {
+ const cfg = hook.response.templateP.template_contract;
+ const def = hook.response.templateP.editable_defaults;
+
+ const fixedAmount = cfg.amount !== undefined ? Amounts.parseOrThrow(cfg.amount) : undefined;
+ const fixedSummary = cfg.summary !== undefined ? cfg.summary : undefined;
+
+ const defaultAmount = def?.amount !== undefined ? Amounts.parseOrThrow(def.amount) : undefined;
+ const defaultSummary = def?.summary !== undefined ? def.summary : undefined;
+
+ const zero = fixedAmount ? Amounts.zeroOfAmount(fixedAmount) :
+ cfg.currency !== undefined ? Amounts.zeroOfCurrency(cfg.currency) :
+ defaultAmount !== undefined ? Amounts.zeroOfAmount(defaultAmount) :
+ def?.currency !== undefined ? Amounts.zeroOfCurrency(def.currency) :
+ Amounts.zeroOfCurrency(hook.response.templateP.supportedCurrencies[0]);
+
+ const [amount, setAmount] = useState(defaultAmount ?? zero);
+ const [summary, setSummary] = useState(defaultSummary ?? "");
+
+ async function createOrder() {
+ try {
+ const templateParams: Record<string, string> = {};
+ if (amount && !fixedAmount) {
+ templateParams["amount"] = Amounts.stringify(amount);
+ }
+ if (summary && !fixedSummary) {
+ templateParams["summary"] = summary;
+ }
+ const payStatus = await api.wallet.call(
+ WalletApiOperation.PreparePayForTemplate,
+ {
+ talerPayTemplateUri: talerTemplateUri,
+ templateParams,
+ },
+ );
+ setNewOrder(payStatus.talerUri!);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ const errors = undefinedIfEmpty({
+ amount: fixedAmount !== undefined ? undefined : amount && Amounts.isZero(amount) ? i18n.str`required` : undefined,
+ summary: fixedSummary !== undefined ? undefined : summary !== undefined && !summary ? i18n.str`required` : undefined,
+ });
+ return {
+ status: "fill-template",
+ error: undefined,
+ minAge: cfg.minimum_age ?? 0,
+ amount:
+ fixedAmount === undefined
+ ? ({
+ onInput: (a) => {
+ setAmount(a);
+ },
+ value: amount,
+ error: errors?.amount,
+ } as AmountFieldHandler)
+ : undefined,
+ summary:
+ fixedSummary === undefined
+ ? ({
+ onInput: (t) => {
+ setSummary(t);
+ },
+ value: summary,
+ error: errors?.summary,
+ } as TextFieldHandler)
+ : undefined,
+ onCreate: {
+ onClick: errors
+ ? undefined
+ : safely("create order for pay template", createOrder),
+ },
+ };
+ }
+
+}
+
+function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, unknown>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx
new file mode 100644
index 000000000..02607fa30
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx
@@ -0,0 +1,34 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "payment template",
+ component: ReadyView,
+ argTypes: {},
+};
+
+export const PaymentPossible = tests.createExample(ReadyView, {
+ status: "fill-template",
+ error: undefined,
+});
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts
new file mode 100644
index 000000000..d15761eae
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
+import { useComponentState } from "./state.js";
+
+describe("Order template CTA states", () => {
+ it("should tell the user that the URI is missing", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerTemplateUri: "",
+ cancel: nullFunction,
+ goToWalletManualWithdraw: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ ({ status, error }) => {
+ expect(status).equals("error");
+ if (error === undefined) expect.fail();
+ // expect(error.hasError).true;
+ // expect(error.operational).false;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
new file mode 100644
index 000000000..ce53c3cf9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx
@@ -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 { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { AmountField } from "../../components/AmountField.js";
+import { Button } from "../../mui/Button.js";
+import { TextField } from "../../mui/TextField.js";
+import { State } from "./index.js";
+import { AgeSign } from "../../components/styled/index.js";
+
+export function ReadyView({
+ amount,
+ minAge,
+ summary,
+ onCreate,
+}: State.FillTemplate): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <section style={{ textAlign: "left" }}>
+ {/* <Part
+ title={
+ <div
+ style={{
+ display: "flex",
+ alignItems: "center",
+ }}
+ >
+ <i18n.Translate>Merchant</i18n.Translate>
+ </div>
+ }
+ text={<ExchangeDetails exchange={exchangeUrl} />}
+ kind="neutral"
+ big
+ /> */}
+ {!amount ? undefined : (
+ <p>
+ <AmountField label={i18n.str`Amount`} handler={amount} />
+ </p>
+ )}
+ {!summary ? undefined : (
+ <p>
+ <TextField
+ label="Summary"
+ variant="filled"
+ required
+ fullWidth
+ error={summary.error}
+ value={summary.value}
+ onChange={summary.onInput}
+ />
+ </p>
+ )}
+ </section>
+ {minAge && (
+ <section>
+ <AgeSign size={25}>{minAge}+</AgeSign>
+ <i18n.Translate>This purchase is age restricted.</i18n.Translate>
+ </section>
+ )}
+ <section>
+ <Button onClick={onCreate.onClick} variant="contained" color="success">
+ <i18n.Translate>Review order</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Recovery/index.ts b/packages/taler-wallet-webextension/src/cta/Recovery/index.ts
new file mode 100644
index 000000000..79056c15b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Recovery/index.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/>
+ */
+
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+
+export interface Props {
+ talerRecoveryUri?: string;
+ onCancel: () => Promise<void>;
+ onSuccess: () => Promise<void>;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ cancel: ButtonHandler;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ accept: ButtonHandler;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ ready: ReadyView,
+};
+
+export const RecoveryPage = compose(
+ "Recovery",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/Recovery/state.ts b/packages/taler-wallet-webextension/src/cta/Recovery/state.ts
new file mode 100644
index 000000000..5399c5bfc
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Recovery/state.ts
@@ -0,0 +1,84 @@
+/*
+ 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 { parseRestoreUri } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerRecoveryUri,
+ onCancel,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
+ if (!talerRecoveryUri) {
+ return {
+ status: "error",
+ error: {
+ type: "error",
+ message: i18n.str`Missing URI`,
+ description: i18n.str``,
+ cause: new Error("something"),
+ context: {},
+ },
+ };
+ }
+ const info = parseRestoreUri(talerRecoveryUri);
+
+ if (!info) {
+ return {
+ status: "error",
+ error: {
+ type: "error",
+ message: i18n.str`Could not parse the recovery URI`,
+ description: i18n.str``,
+ cause: new Error("something"),
+ context: {},
+ },
+ };
+ }
+ const recovery = info;
+
+ async function recoverBackup(): Promise<void> {
+ await api.wallet.call(WalletApiOperation.ImportBackupRecovery, {
+ recovery: {
+ walletRootPriv: recovery.walletRootPriv,
+ providers: recovery.providers.map((url) => ({
+ name: new URL(url).hostname,
+ url,
+ })),
+ },
+ });
+ onSuccess();
+ }
+
+ return {
+ status: "ready",
+
+ accept: {
+ onClick: pushAlertOnError(recoverBackup),
+ },
+ cancel: {
+ onClick: pushAlertOnError(onCancel),
+ },
+ error: undefined,
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx b/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx
new file mode 100644
index 000000000..4d8dc3737
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Recovery/stories.tsx
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "recovery",
+};
diff --git a/packages/taler-wallet-webextension/src/permissions.ts b/packages/taler-wallet-webextension/src/cta/Recovery/test.ts
index bcd357fd6..68c75b380 100644
--- a/packages/taler-wallet-webextension/src/permissions.ts
+++ b/packages/taler-wallet-webextension/src/cta/Recovery/test.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,7 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-export const extendedPermissions = {
- permissions: ["webRequest", "webRequestBlocking"],
- origins: ["http://*/*", "https://*/*"],
-};
+describe("Backup import CTA states", () => {
+ it.skip("should test something", async () => {
+ return;
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/Recovery/views.tsx b/packages/taler-wallet-webextension/src/cta/Recovery/views.tsx
new file mode 100644
index 000000000..5a3a00daa
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Recovery/views.tsx
@@ -0,0 +1,41 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Fragment, h, VNode } from "preact";
+import { LogoHeader } from "../../components/LogoHeader.js";
+import { SubTitle, WalletAction } from "../../components/styled/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Button } from "../../mui/Button.js";
+import { State } from "./index.js";
+
+export function ReadyView({ accept, cancel }: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <section>
+ <p>
+ <i18n.Translate>Import backup, show info</i18n.Translate>
+ </p>
+ <Button variant="contained" onClick={accept.onClick}>
+ Import
+ </Button>
+ <Button variant="contained" onClick={cancel.onClick}>
+ Cancel
+ </Button>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
deleted file mode 100644
index 88e714cb7..000000000
--- a/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { OrderShortInfo } from '@gnu-taler/taler-util';
-import { createExample } from '../test-utils';
-import { View as TestedComponent } from './Refund';
-
-
-export default {
- title: 'cta/refund',
- component: TestedComponent,
- argTypes: {
- },
-};
-
-export const Complete = createExample(TestedComponent, {
- applyResult: {
- amountEffectivePaid: 'USD:10',
- amountRefundGone: 'USD:0',
- amountRefundGranted: 'USD:2',
- contractTermsHash: 'QWEASDZXC',
- info: {
- summary: 'tasty cold beer',
- contractTermsHash: 'QWEASDZXC',
- } as Partial<OrderShortInfo> as any,
- pendingAtExchange: false,
- proposalId: "proposal123",
- }
-});
-
-export const Partial = createExample(TestedComponent, {
- applyResult: {
- amountEffectivePaid: 'USD:10',
- amountRefundGone: 'USD:1',
- amountRefundGranted: 'USD:2',
- contractTermsHash: 'QWEASDZXC',
- info: {
- summary: 'tasty cold beer',
- contractTermsHash: 'QWEASDZXC',
- } as Partial<OrderShortInfo> as any,
- pendingAtExchange: false,
- proposalId: "proposal123",
- }
-});
-
-export const InProgress = createExample(TestedComponent, {
- applyResult: {
- amountEffectivePaid: 'USD:10',
- amountRefundGone: 'USD:1',
- amountRefundGranted: 'USD:2',
- contractTermsHash: 'QWEASDZXC',
- info: {
- summary: 'tasty cold beer',
- contractTermsHash: 'QWEASDZXC',
- } as Partial<OrderShortInfo> as any,
- pendingAtExchange: true,
- proposalId: "proposal123",
- }
-});
diff --git a/packages/taler-wallet-webextension/src/cta/Refund.tsx b/packages/taler-wallet-webextension/src/cta/Refund.tsx
deleted file mode 100644
index 943095360..000000000
--- a/packages/taler-wallet-webextension/src/cta/Refund.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- This file is part of TALER
- (C) 2015-2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Page that shows refund status for purchases.
- *
- * @author Florian Dold
- */
-
-import * as wxApi from "../wxApi";
-import { AmountView } from "../renderHtml";
-import {
- ApplyRefundResponse,
- Amounts,
-} from "@gnu-taler/taler-util";
-import { useEffect, useState } from "preact/hooks";
-import { JSX } from "preact/jsx-runtime";
-import { h } from 'preact';
-
-interface Props {
- talerRefundUri?: string
-}
-export interface ViewProps {
- applyResult: ApplyRefundResponse;
-}
-export function View({ applyResult }: ViewProps) {
- return <section class="main">
- <h1>GNU Taler Wallet</h1>
- <article class="fade">
- <h2>Refund Status</h2>
- <p>
- The product <em>{applyResult.info.summary}</em> has received a total
- effective refund of{" "}
- <AmountView amount={applyResult.amountRefundGranted} />.
- </p>
- {applyResult.pendingAtExchange ? (
- <p>Refund processing is still in progress.</p>
- ) : null}
- {!Amounts.isZero(applyResult.amountRefundGone) ? (
- <p>
- The refund amount of{" "}
- <AmountView amount={applyResult.amountRefundGone} />{" "}
- could not be applied.
- </p>
- ) : null}
- </article>
- </section>
-}
-export function RefundPage({ talerRefundUri }: Props): JSX.Element {
- const [applyResult, setApplyResult] = useState<ApplyRefundResponse | undefined>(undefined);
- const [errMsg, setErrMsg] = useState<string | undefined>(undefined);
-
- useEffect(() => {
- if (!talerRefundUri) return;
- const doFetch = async (): Promise<void> => {
- try {
- const result = await wxApi.applyRefund(talerRefundUri);
- setApplyResult(result);
- } catch (e) {
- console.error(e);
- setErrMsg(e.message);
- console.log("err message", e.message);
- }
- };
- doFetch();
- }, [talerRefundUri]);
-
- console.log("rendering");
-
- if (!talerRefundUri) {
- return <span>missing taler refund uri</span>;
- }
-
- if (errMsg) {
- return <span>Error: {errMsg}</span>;
- }
-
- if (!applyResult) {
- return <span>Updating refund status</span>;
- }
-
- return <View applyResult={applyResult} />;
-}
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/index.ts b/packages/taler-wallet-webextension/src/cta/Refund/index.ts
new file mode 100644
index 000000000..42e9cc534
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Refund/index.ts
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AmountJson, Product } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { IgnoredView, ReadyView } from "./views.js";
+
+export interface Props {
+ talerRefundUri?: string;
+ cancel: () => Promise<void>;
+ onSuccess: (tx: string) => Promise<void>;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | State.Ready
+ // | State.InProgress
+ | State.Ignored;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ interface BaseInfo {
+ merchantName: string;
+ // products: Product[] | undefined;
+ amount: AmountJson;
+ // awaitingAmount: AmountJson;
+ // granted: AmountJson;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+
+ accept: ButtonHandler;
+ ignore: ButtonHandler;
+ orderId: string;
+ cancel: () => Promise<void>;
+ }
+
+ export interface Ignored extends BaseInfo {
+ status: "ignored";
+ error: undefined;
+ }
+ // export interface InProgress extends BaseInfo {
+ // status: "in-progress";
+ // error: undefined;
+ // }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ // "in-progress": InProgressView,
+ ignored: IgnoredView,
+ ready: ReadyView,
+};
+
+export const RefundPage = compose(
+ "Refund",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/state.ts b/packages/taler-wallet-webextension/src/cta/Refund/state.ts
new file mode 100644
index 000000000..6f0a98151
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Refund/state.ts
@@ -0,0 +1,141 @@
+/*
+ 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,
+ NotificationType,
+ TransactionPayment,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useEffect, useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerRefundUri,
+ cancel,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const [ignored, setIgnored] = useState(false);
+ const { pushAlertOnError } = useAlertContext();
+
+ const info = useAsyncAsHook(async () => {
+ if (!talerRefundUri) throw Error("ERROR_NO-URI-FOR-REFUND");
+ const refund = await api.wallet.call(
+ WalletApiOperation.StartRefundQueryForUri,
+ {
+ talerRefundUri,
+ },
+ );
+ const purchase = await api.wallet.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: refund.transactionId,
+ },
+ );
+ if (purchase.type !== TransactionType.Payment) {
+ throw Error("Refund of non purchase transaction is not handled");
+ }
+ return { refund, purchase, uri: talerRefundUri };
+ });
+
+ useEffect(() =>
+ api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
+ info?.retry,
+ ),
+ );
+
+ if (!info) {
+ return { status: "loading", error: undefined };
+ }
+ if (info.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the refund status`,
+ info,
+ ),
+ };
+ }
+ // if (info.hasError) {
+ // return {
+ // status: "loading-uri",
+ // error: info,
+ // };
+ // }
+
+ const { refund, purchase, uri } = info.response;
+
+ const doAccept = async (): Promise<void> => {
+ const res = await api.wallet.call(
+ WalletApiOperation.StartRefundQueryForUri,
+ {
+ talerRefundUri: uri,
+ },
+ );
+
+ onSuccess(res.transactionId);
+ };
+
+ const doIgnore = async (): Promise<void> => {
+ setIgnored(true);
+ };
+
+ const baseInfo = {
+ amount: Amounts.parseOrThrow(purchase.amountEffective),
+ // granted: Amounts.parseOrThrow(info.response.refund.granted),
+ // awaitingAmount: Amounts.parseOrThrow(refund.awaiting),
+ merchantName: purchase.info.merchant.name,
+ // products: purchase.info.products,
+ error: undefined,
+ };
+
+ if (ignored) {
+ return {
+ status: "ignored",
+ ...baseInfo,
+ };
+ }
+
+ //FIXME: DD37 wallet-core is not returning this value
+ // if (refund.pending) {
+ // return {
+ // status: "in-progress",
+ // ...baseInfo,
+ // };
+ // }
+
+ return {
+ status: "ready",
+ ...baseInfo,
+ orderId: purchase.info.orderId,
+ accept: {
+ onClick: pushAlertOnError(doAccept),
+ },
+ ignore: {
+ onClick: pushAlertOnError(doIgnore),
+ },
+ cancel,
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx b/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx
new file mode 100644
index 000000000..03d55ee91
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Refund/stories.tsx
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import beer from "../../../static-dev/beer.png";
+import * as tests from "@gnu-taler/web-util/testing";
+import { IgnoredView, ReadyView } from "./views.js";
+export default {
+ title: "refund",
+};
+
+// export const InProgress = tests.createExample(InProgressView, {
+// status: "in-progress",
+// error: undefined,
+// amount: Amounts.parseOrThrow("USD:1"),
+// awaitingAmount: Amounts.parseOrThrow("USD:1"),
+// granted: Amounts.parseOrThrow("USD:0"),
+// merchantName: "the merchant",
+// products: undefined,
+// });
+
+export const Ready = tests.createExample(ReadyView, {
+ status: "ready",
+ error: undefined,
+ accept: {},
+ ignore: {},
+
+ amount: Amounts.parseOrThrow("USD:1"),
+ // awaitingAmount: Amounts.parseOrThrow("USD:1"),
+ // granted: Amounts.parseOrThrow("USD:0"),
+ merchantName: "the merchant",
+ // products: [],
+ orderId: "abcdef",
+});
+
+export const WithAProductList = tests.createExample(ReadyView, {
+ status: "ready",
+ error: undefined,
+ accept: {},
+ ignore: {},
+ amount: Amounts.parseOrThrow("USD:1"),
+ // awaitingAmount: Amounts.parseOrThrow("USD:1"),
+ // granted: Amounts.parseOrThrow("USD:0"),
+ merchantName: "the merchant",
+ // products: [
+ // {
+ // description: "beer",
+ // image: beer,
+ // quantity: 2,
+ // },
+ // {
+ // description: "t-shirt",
+ // price: "EUR:1",
+ // quantity: 5,
+ // },
+ // ],
+ orderId: "abcdef",
+});
+
+export const Ignored = tests.createExample(IgnoredView, {
+ status: "ignored",
+ error: undefined,
+ merchantName: "the merchant",
+});
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/test.ts b/packages/taler-wallet-webextension/src/cta/Refund/test.ts
new file mode 100644
index 000000000..bc0e61fcb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Refund/test.ts
@@ -0,0 +1,287 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ Amounts,
+ NotificationType,
+ OrderShortInfo,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { expect } from "chai";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
+import { useComponentState } from "./state.js";
+
+/**
+ * Commenting this tests out since the behavior
+ */
+
+describe("Refund CTA states", () => {
+ // it("should tell the user that the URI is missing", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+ // const props = {
+ // talerRefundUri: undefined,
+ // cancel: nullFunction,
+ // onSuccess: nullFunction,
+ // };
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // ({ status, error }) => {
+ // expect(status).equals("loading");
+ // expect(error).undefined;
+ // },
+ // ({ status, error }) => {
+ // expect(status).equals("error");
+ // if (!error) expect.fail();
+ // // if (!error.hasError) expect.fail();
+ // // if (error.operational) expect.fail();
+ // expect(error.description).eq("ERROR_NO-URI-FOR-REFUND");
+ // },
+ // ],
+ // TestingContext,
+ // );
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+ // it("should be ready after loading", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+ // const props = {
+ // talerRefundUri: "taler://refund/asdasdas",
+ // cancel: nullFunction,
+ // onSuccess: nullFunction,
+ // };
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.StartRefundQueryForUri,
+ // undefined,
+ // {
+ // // awaiting: "EUR:2",
+ // // effectivePaid: "EUR:2",
+ // // gone: "EUR:0",
+ // // granted: "EUR:0",
+ // // pending: false,
+ // // proposalId: "1",
+ // // info: {
+ // // contractTermsHash: "123",
+ // // merchant: {
+ // // name: "the merchant name",
+ // // },
+ // // orderId: "orderId1",
+ // // summary: "the summary",
+ // // } as OrderShortInfo,
+ // },
+ // );
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // ({ status, error }) => {
+ // expect(status).equals("loading");
+ // expect(error).undefined;
+ // },
+ // (state) => {
+ // if (state.status !== "ready") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.accept.onClick).not.undefined;
+ // expect(state.ignore.onClick).not.undefined;
+ // expect(state.merchantName).eq("the merchant name");
+ // expect(state.orderId).eq("orderId1");
+ // expect(state.products).undefined;
+ // },
+ // ],
+ // TestingContext,
+ // );
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+ // it("should be ignored after clicking the ignore button", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+ // const props = {
+ // talerRefundUri: "taler://refund/asdasdas",
+ // cancel: async () => {
+ // null;
+ // },
+ // onSuccess: async () => {
+ // null;
+ // },
+ // };
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.StartRefundQueryForUri,
+ // undefined,
+ // {
+ // // awaiting: "EUR:2",
+ // // effectivePaid: "EUR:2",
+ // // gone: "EUR:0",
+ // // granted: "EUR:0",
+ // // pending: false,
+ // // proposalId: "1",
+ // // info: {
+ // // contractTermsHash: "123",
+ // // merchant: {
+ // // name: "the merchant name",
+ // // },
+ // // orderId: "orderId1",
+ // // summary: "the summary",
+ // // } as OrderShortInfo,
+ // },
+ // );
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // ({ status, error }) => {
+ // expect(status).equals("loading");
+ // expect(error).undefined;
+ // },
+ // (state) => {
+ // if (state.status !== "ready") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.accept.onClick).not.undefined;
+ // expect(state.merchantName).eq("the merchant name");
+ // expect(state.orderId).eq("orderId1");
+ // expect(state.products).undefined;
+ // if (state.ignore.onClick === undefined) expect.fail();
+ // state.ignore.onClick();
+ // },
+ // (state) => {
+ // if (state.status !== "ignored") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.merchantName).eq("the merchant name");
+ // },
+ // ],
+ // TestingContext,
+ // );
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+ // it("should be in progress when doing refresh", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+ // const props = {
+ // talerRefundUri: "taler://refund/asdasdas",
+ // cancel: async () => {
+ // null;
+ // },
+ // onSuccess: async () => {
+ // null;
+ // },
+ // };
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.StartRefundQueryForUri,
+ // undefined,
+ // {
+ // // awaiting: "EUR:2",
+ // // effectivePaid: "EUR:2",
+ // // gone: "EUR:0",
+ // // granted: "EUR:0",
+ // // pending: true,
+ // // proposalId: "1",
+ // // info: {
+ // // contractTermsHash: "123",
+ // // merchant: {
+ // // name: "the merchant name",
+ // // },
+ // // orderId: "orderId1",
+ // // summary: "the summary",
+ // // } as OrderShortInfo,
+ // },
+ // );
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.StartRefundQueryForUri,
+ // undefined,
+ // {
+ // // awaiting: "EUR:1",
+ // // effectivePaid: "EUR:2",
+ // // gone: "EUR:0",
+ // // granted: "EUR:1",
+ // // pending: true,
+ // // proposalId: "1",
+ // // info: {
+ // // contractTermsHash: "123",
+ // // merchant: {
+ // // name: "the merchant name",
+ // // },
+ // // orderId: "orderId1",
+ // // summary: "the summary",
+ // // } as OrderShortInfo,
+ // },
+ // );
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.StartRefundQueryForUri,
+ // undefined,
+ // {
+ // // awaiting: "EUR:0",
+ // // effectivePaid: "EUR:2",
+ // // gone: "EUR:0",
+ // // granted: "EUR:2",
+ // // pending: false,
+ // // proposalId: "1",
+ // // info: {
+ // // contractTermsHash: "123",
+ // // merchant: {
+ // // name: "the merchant name",
+ // // },
+ // // orderId: "orderId1",
+ // // summary: "the summary",
+ // // } as OrderShortInfo,
+ // },
+ // );
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // ({ status, error }) => {
+ // expect(status).equals("loading");
+ // expect(error).undefined;
+ // },
+ // (state) => {
+ // if (state.status !== "in-progress") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.merchantName).eq("the merchant name");
+ // expect(state.products).undefined;
+ // expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
+ // // expect(state.progress).closeTo(1 / 3, 0.01)
+ // handler.notifyEventFromWallet(NotificationType.TransactionStateTransition);
+ // },
+ // (state) => {
+ // if (state.status !== "in-progress") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.merchantName).eq("the merchant name");
+ // expect(state.products).undefined;
+ // expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
+ // // expect(state.progress).closeTo(2 / 3, 0.01)
+ // handler.notifyEventFromWallet(NotificationType.TransactionStateTransition);
+ // },
+ // (state) => {
+ // if (state.status !== "ready") expect.fail();
+ // if (state.error) expect.fail();
+ // expect(state.merchantName).eq("the merchant name");
+ // expect(state.products).undefined;
+ // expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
+ // },
+ // ],
+ // TestingContext,
+ // );
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
new file mode 100644
index 000000000..ae4d728f3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
@@ -0,0 +1,123 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Amount } from "../../components/Amount.js";
+import { Part } from "../../components/Part.js";
+import { Button } from "../../mui/Button.js";
+import { State } from "./index.js";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
+
+export function IgnoredView(state: State.Ignored): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <section>
+ <p>
+ <i18n.Translate>You&apos;ve ignored the refund.</i18n.Translate>
+ </p>
+ </section>
+ </Fragment>
+ );
+}
+// export function InProgressView(state: State.InProgress): VNode {
+// const { i18n } = useTranslationContext();
+
+// return (
+// <Fragment>
+// <section>
+// <p>
+// <i18n.Translate>The refund is in progress.</i18n.Translate>
+// </p>
+// </section>
+// <section>
+// <Part
+// big
+// title={i18n.str`Total to refund`}
+// text={<Amount value={state.awaitingAmount} />}
+// kind="negative"
+// />
+// <Part
+// big
+// title={i18n.str`Refunded`}
+// text={<Amount value={state.amount} />}
+// kind="negative"
+// />
+// </section>
+// {state.products && state.products.length ? (
+// <section>
+// <ProductList products={state.products} />
+// </section>
+// ) : undefined}
+// </Fragment>
+// );
+// }
+export function ReadyView(state: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <section>
+ <p>
+ <i18n.Translate>
+ The merchant &quot;<b>{state.merchantName}</b>&quot; is offering you
+ a refund.
+ </i18n.Translate>
+ </p>
+ </section>
+ <section>
+ <Part
+ big
+ title={i18n.str`Order amount`}
+ text={<Amount value={state.amount} />}
+ kind="neutral"
+ />
+ {/* {Amounts.isNonZero(state.granted) && (
+ <Part
+ big
+ title={i18n.str`Already refunded`}
+ text={<Amount value={state.granted} />}
+ kind="neutral"
+ />
+ )}
+ <Part
+ big
+ title={i18n.str`Refund offered (without fee)`}
+ text={<Amount value={state.awaitingAmount} />}
+ kind="positive"
+ /> */}
+ </section>
+ {/* {state.products && state.products.length ? (
+ <section>
+ <ProductList products={state.products} />
+ </section>
+ ) : undefined} */}
+ <section>
+ <Button
+ variant="contained"
+ color="success"
+ onClick={state.accept.onClick}
+ >
+ <i18n.Translate>
+ {/* Accept &nbsp; <Amount value={state.awaitingAmount} /> */}
+ Accept
+ </i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx b/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx
deleted file mode 100644
index 389b183f0..000000000
--- a/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample } from '../test-utils';
-import { View as TestedComponent } from './Tip';
-
-
-export default {
- title: 'cta/tip',
- component: TestedComponent,
- argTypes: {
- },
-};
-
-export const Accepted = createExample(TestedComponent, {
- prepareTipResult: {
- accepted: true,
- merchantBaseUrl: '',
- exchangeBaseUrl: '',
- expirationTimestamp : {
- t_ms: 0
- },
- tipAmountEffective: 'USD:10',
- tipAmountRaw: 'USD:5',
- walletTipId: 'id'
- }
-});
-
-export const NotYetAccepted = createExample(TestedComponent, {
- prepareTipResult: {
- accepted: false,
- merchantBaseUrl: 'http://merchant.url/',
- exchangeBaseUrl: 'http://exchange.url/',
- expirationTimestamp : {
- t_ms: 0
- },
- tipAmountEffective: 'USD:10',
- tipAmountRaw: 'USD:5',
- walletTipId: 'id'
- }
-});
diff --git a/packages/taler-wallet-webextension/src/cta/Tip.tsx b/packages/taler-wallet-webextension/src/cta/Tip.tsx
deleted file mode 100644
index dc1feaed3..000000000
--- a/packages/taler-wallet-webextension/src/cta/Tip.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- This file is part of TALER
- (C) 2017 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Page shown to the user to accept or ignore a tip from a merchant.
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-import { useEffect, useState } from "preact/hooks";
-import { PrepareTipResult } from "@gnu-taler/taler-util";
-import { AmountView } from "../renderHtml";
-import * as wxApi from "../wxApi";
-import { JSX } from "preact/jsx-runtime";
-import { h } from 'preact';
-
-interface Props {
- talerTipUri?: string
-}
-export interface ViewProps {
- prepareTipResult: PrepareTipResult;
- onAccept: () => void;
- onIgnore: () => void;
-
-}
-export function View({ prepareTipResult, onAccept, onIgnore }: ViewProps) {
- return <section class="main">
- <h1>GNU Taler Wallet</h1>
- <article class="fade">
- {prepareTipResult.accepted ? (
- <span>
- Tip from <code>{prepareTipResult.merchantBaseUrl}</code> accepted. Check
- your transactions list for more details.
- </span>
- ) : (
- <div>
- <p>
- The merchant <code>{prepareTipResult.merchantBaseUrl}</code> is
- offering you a tip of{" "}
- <strong>
- <AmountView amount={prepareTipResult.tipAmountEffective} />
- </strong>{" "}
- via the exchange <code>{prepareTipResult.exchangeBaseUrl}</code>
- </p>
- <button onClick={onAccept}>Accept tip</button>
- <button onClick={onIgnore}>Ignore</button>
- </div>
- )}
- </article>
- </section>
-
-}
-
-export function TipPage({ talerTipUri }: Props): JSX.Element {
- const [updateCounter, setUpdateCounter] = useState<number>(0);
- const [prepareTipResult, setPrepareTipResult] = useState<
- PrepareTipResult | undefined
- >(undefined);
-
- const [tipIgnored, setTipIgnored] = useState(false);
-
- useEffect(() => {
- if (!talerTipUri) return;
- const doFetch = async (): Promise<void> => {
- const p = await wxApi.prepareTip({ talerTipUri });
- setPrepareTipResult(p);
- };
- doFetch();
- }, [talerTipUri, updateCounter]);
-
- const doAccept = async () => {
- if (!prepareTipResult) {
- return;
- }
- await wxApi.acceptTip({ walletTipId: prepareTipResult?.walletTipId });
- setUpdateCounter(updateCounter + 1);
- };
-
- const doIgnore = () => {
- setTipIgnored(true);
- };
-
- if (!talerTipUri) {
- return <span>missing tip uri</span>;
- }
-
- if (tipIgnored) {
- return <span>You've ignored the tip.</span>;
- }
-
- if (!prepareTipResult) {
- return <span>Loading ...</span>;
- }
-
- return <View prepareTipResult={prepareTipResult}
- onAccept={doAccept} onIgnore={doIgnore}
- />
-}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
new file mode 100644
index 000000000..794d2ad1c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AmountJson, AmountString, TalerErrorDetail } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler, TextFieldHandler } from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+
+export interface Props {
+ amount: AmountString;
+ onClose: () => Promise<void>;
+ onSuccess: (tx: string) => Promise<void>;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ cancel: ButtonHandler;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ create: ButtonHandler;
+ toBeReceived: AmountJson;
+ debitAmount: AmountJson;
+ subject: TextFieldHandler;
+ expiration: TextFieldHandler;
+ error: undefined;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ ready: ReadyView,
+};
+
+export const TransferCreatePage = compose(
+ "TransferCreatePage",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
new file mode 100644
index 000000000..f092801ed
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
@@ -0,0 +1,185 @@
+/*
+ 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 {
+ AmountString,
+ Amounts,
+ TalerErrorCode,
+ TalerProtocolTimestamp,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { isFuture, parse } from "date-fns";
+import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { BackgroundError, WxApiType } from "../../wxApi.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ amount: amountStr,
+ onClose,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const amount = Amounts.parseOrThrow(amountStr);
+ const { i18n } = useTranslationContext();
+
+ const [subject, setSubject] = useState<string | undefined>();
+ const [timestamp, setTimestamp] = useState<string | undefined>();
+
+ const hook = useAsyncAsHook(async () => {
+ const resp = await checkPeerPushDebitAndCheckMax(api, amountStr);
+ return resp;
+ });
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the max amount to transfer`,
+ hook,
+ ),
+ };
+ }
+
+ const { amountEffective, amountRaw } = hook.response;
+ const debitAmount = Amounts.parseOrThrow(amountEffective);
+ const toBeReceived = Amounts.parseOrThrow(amountRaw);
+
+ let purse_expiration: TalerProtocolTimestamp | undefined = undefined;
+ let timestampError: string | undefined = undefined;
+
+ const t =
+ timestamp === undefined
+ ? undefined
+ : parse(timestamp, "dd/MM/yyyy", new Date());
+
+ if (t !== undefined) {
+ if (Number.isNaN(t.getTime())) {
+ timestampError = 'Should have the format "dd/MM/yyyy"';
+ } else {
+ if (!isFuture(t)) {
+ timestampError = "Should be in the future";
+ } else {
+ purse_expiration = {
+ t_s: t.getTime() / 1000,
+ };
+ }
+ }
+ }
+
+ async function accept(): Promise<void> {
+ if (!subject || !purse_expiration) return;
+ const resp = await api.wallet.call(
+ WalletApiOperation.InitiatePeerPushDebit,
+ {
+ partialContractTerms: {
+ summary: subject,
+ amount: amountStr,
+ purse_expiration,
+ },
+ },
+ );
+ onSuccess(resp.transactionId);
+ }
+
+ const unableToCreate =
+ !subject || Amounts.isZero(amount) || !purse_expiration;
+
+ return {
+ status: "ready",
+ cancel: {
+ onClick: pushAlertOnError(onClose),
+ },
+ subject: {
+ error:
+ subject === undefined
+ ? undefined
+ : !subject
+ ? "Can't be empty"
+ : undefined,
+ value: subject ?? "",
+ onInput: pushAlertOnError(async (e) => setSubject(e)),
+ },
+ expiration: {
+ error: timestampError,
+ value: timestamp === undefined ? "" : timestamp,
+ onInput: pushAlertOnError(async (e) => {
+ setTimestamp(e);
+ }),
+ },
+ create: {
+ onClick: unableToCreate ? undefined : pushAlertOnError(accept),
+ },
+ debitAmount,
+ toBeReceived,
+ error: undefined,
+ };
+}
+
+async function checkPeerPushDebitAndCheckMax(
+ api: WxApiType,
+ amountState: AmountString,
+) {
+ // FIXME : https://bugs.gnunet.org/view.php?id=7872
+ try {
+ return await api.wallet.call(WalletApiOperation.CheckPeerPushDebit, {
+ amount: amountState,
+ });
+ } catch (e) {
+ if (!(e instanceof BackgroundError)) {
+ throw e;
+ }
+ if (
+ !e.hasErrorCode(
+ TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+ )
+ ) {
+ throw e;
+ }
+ const material = Amounts.parseOrThrow(
+ e.errorDetail.insufficientBalanceDetails.balanceMaterial,
+ );
+ const amount = Amounts.parseOrThrow(amountState);
+ const gap = Amounts.sub(
+ amount,
+ Amounts.parseOrThrow(
+ e.errorDetail.insufficientBalanceDetails.maxEffectiveSpendAmount,
+ ),
+ ).amount;
+ const newAmount = Amounts.sub(material, gap).amount;
+ if (Amounts.cmp(newAmount, amount) === 0) {
+ //insufficient balance and the exception didn't give
+ //a good response that allow us to try again
+ throw e;
+ }
+ if (Amounts.cmp(newAmount, amount) === 1) {
+ //how can this happen?
+ throw e;
+ }
+ return checkPeerPushDebitAndCheckMax(api, Amounts.stringify(newAmount));
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
new file mode 100644
index 000000000..8e9fbbe63
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
@@ -0,0 +1,50 @@
+/*
+ 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 { nullFunction } from "../../mui/handlers.js";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "transfer create",
+};
+
+export const Ready = tests.createExample(ReadyView, {
+ debitAmount: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ },
+ expiration: {
+ value: "20/1/2022",
+ },
+ create: {},
+ cancel: {},
+ toBeReceived: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ },
+ subject: {
+ value: "the subject",
+ onInput: nullFunction,
+ },
+});
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/test.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/test.ts
new file mode 100644
index 000000000..be753e492
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/test.ts
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("Transfer create states", () => {
+ it.skip("should assert", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
new file mode 100644
index 000000000..bc855f33d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx
@@ -0,0 +1,125 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { Part } from "../../components/Part.js";
+import { Button } from "../../mui/Button.js";
+import { TextField } from "../../mui/TextField.js";
+import {
+ getAmountWithFee,
+ TransferCreationDetails,
+} from "../../wallet/Transaction.js";
+import { State } from "./index.js";
+
+export function ReadyView({
+ subject,
+ expiration,
+ toBeReceived,
+ debitAmount,
+ create,
+}: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+
+ async function oneDayExpiration() {
+ if (expiration.onInput) {
+ expiration.onInput(
+ format(new Date().getTime() + 1000 * 60 * 60 * 24, "dd/MM/yyyy"),
+ );
+ }
+ }
+
+ async function oneWeekExpiration() {
+ if (expiration.onInput) {
+ expiration.onInput(
+ format(new Date().getTime() + 1000 * 60 * 60 * 24 * 7, "dd/MM/yyyy"),
+ );
+ }
+ }
+ async function _30DaysExpiration() {
+ if (expiration.onInput) {
+ expiration.onInput(
+ format(new Date().getTime() + 1000 * 60 * 60 * 24 * 30, "dd/MM/yyyy"),
+ );
+ }
+ }
+ return (
+ <Fragment>
+ <section style={{ textAlign: "left" }}>
+ <p>
+ <TextField
+ label="Subject"
+ variant="filled"
+ helperText={i18n.str`Short description of the transfer`}
+ error={subject.error}
+ required
+ fullWidth
+ value={subject.value}
+ onChange={subject.onInput}
+ />
+ </p>
+ <p>
+ <TextField
+ label="Expiration"
+ variant="filled"
+ error={expiration.error}
+ required
+ fullWidth
+ value={expiration.value}
+ onChange={expiration.onInput}
+ />
+ <p>
+ <Button
+ variant="outlined"
+ disabled={!expiration.onInput}
+ onClick={oneDayExpiration}
+ >
+ 1 day
+ </Button>
+ <Button
+ variant="outlined"
+ disabled={!expiration.onInput}
+ onClick={oneWeekExpiration}
+ >
+ 1 week
+ </Button>
+ <Button
+ variant="outlined"
+ disabled={!expiration.onInput}
+ onClick={_30DaysExpiration}
+ >
+ 30 days
+ </Button>
+ </p>
+ </p>
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <TransferCreationDetails
+ amount={getAmountWithFee(debitAmount, toBeReceived, "debit")}
+ />
+ }
+ />
+ </section>
+ <section>
+ <Button onClick={create.onClick} variant="contained" color="success">
+ <i18n.Translate>Create</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts
new file mode 100644
index 000000000..4e1301d6a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts
@@ -0,0 +1,75 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ TalerErrorDetail,
+} from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ButtonHandler } from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+
+export interface Props {
+ talerPayPushUri: string;
+ onClose: () => Promise<void>;
+ onSuccess: (tx: string) => Promise<void>;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ cancel: ButtonHandler;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ effective: AmountJson;
+ exchangeBaseUrl: string;
+ raw: AmountJson;
+ summary: string | undefined;
+ expiration: AbsoluteTime | undefined;
+ error: undefined;
+ accept: ButtonHandler;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ ready: ReadyView,
+};
+
+export const TransferPickupPage = compose(
+ "TransferPickupPage",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
new file mode 100644
index 000000000..67f6d9113
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
@@ -0,0 +1,99 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ Amounts,
+ TalerProtocolTimestamp,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ talerPayPushUri,
+ onClose,
+ onSuccess,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
+ const hook = useAsyncAsHook(async () => {
+ return await api.wallet.call(WalletApiOperation.PreparePeerPushCredit, {
+ talerUri: talerPayPushUri,
+ });
+ }, []);
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the invoice payment status`,
+ hook,
+ ),
+ };
+ }
+
+ const {
+ contractTerms,
+ transactionId,
+ amountEffective,
+ amountRaw,
+ exchangeBaseUrl,
+ } = hook.response;
+
+ const effective = Amounts.parseOrThrow(amountEffective);
+ const raw = Amounts.parseOrThrow(amountRaw);
+ const summary: string = contractTerms.summary;
+ const expiration: TalerProtocolTimestamp = contractTerms.purse_expiration;
+
+ async function accept(): Promise<void> {
+ const resp = await api.wallet.call(
+ WalletApiOperation.ConfirmPeerPushCredit,
+ {
+ transactionId,
+ },
+ );
+ onSuccess(resp.transactionId);
+ }
+ return {
+ status: "ready",
+ effective,
+ exchangeBaseUrl,
+ raw,
+ error: undefined,
+ accept: {
+ onClick: pushAlertOnError(accept),
+ },
+ summary,
+ expiration: expiration
+ ? AbsoluteTime.fromProtocolTimestamp(expiration)
+ : undefined,
+ cancel: {
+ onClick: pushAlertOnError(onClose),
+ },
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx b/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx
new file mode 100644
index 000000000..4fb230cd9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx
@@ -0,0 +1,47 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+
+export default {
+ title: "transfer pickup",
+};
+
+export const Ready = tests.createExample(ReadyView, {
+ effective: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ },
+ raw: {
+ currency: "ARS",
+ value: 1,
+ fraction: 0,
+ },
+ summary: "some subject",
+ expiration: AbsoluteTime.fromMilliseconds(
+ new Date().getTime() + 1000 * 60 * 60,
+ ),
+ accept: {},
+ cancel: {},
+});
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/test.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/test.ts
new file mode 100644
index 000000000..fa5b6979a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/test.ts
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("Transfer pickup states", () => {
+ it.skip("should assert", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
new file mode 100644
index 000000000..caa1b485a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+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 { Time } from "../../components/Time.js";
+import { Button } from "../../mui/Button.js";
+import {
+ getAmountWithFee,
+ TransferPickupDetails,
+} from "../../wallet/Transaction.js";
+import { State } from "./index.js";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
+
+export function ReadyView({
+ accept,
+ summary,
+ expiration,
+ effective,
+ exchangeBaseUrl,
+ raw,
+}: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <section style={{ textAlign: "left" }}>
+ <Part title={i18n.str`Subject`} text={<div>{summary}</div>} />
+ <Part title={i18n.str`Amount`} text={<Amount value={raw} />} />
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <TransferPickupDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
+ />
+ }
+ />
+
+ <Part
+ title={i18n.str`Valid until`}
+ text={<Time timestamp={expiration} format="dd MMMM yyyy, HH:mm" />}
+ kind="neutral"
+ />
+ </section>
+ <section>
+ <TermsOfService key="terms" exchangeUrl={exchangeBaseUrl} >
+ <Button variant="contained" color="success" onClick={accept.onClick}>
+ <i18n.Translate>
+ Receive &nbsp; {<Amount value={effective} />}
+ </i18n.Translate>
+ </Button>
+ </TermsOfService>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
deleted file mode 100644
index 6ef72cbe6..000000000
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
+++ /dev/null
@@ -1,383 +0,0 @@
-/*
- This file is part of TALER
- (C) 2015-2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Page shown to the user to confirm creation
- * of a reserve, usually requested by the bank.
- *
- * @author Florian Dold
- */
-
-import { AmountJson, Amounts, ExchangeListItem, GetExchangeTosResult, i18n, WithdrawUriInfoResponse } from '@gnu-taler/taler-util';
-import { ExchangeWithdrawDetails } from '@gnu-taler/taler-wallet-core/src/operations/withdraw';
-import { useState } from "preact/hooks";
-import { Fragment } from 'preact/jsx-runtime';
-import { CheckboxOutlined } from '../components/CheckboxOutlined';
-import { ExchangeXmlTos } from '../components/ExchangeToS';
-import { LogoHeader } from '../components/LogoHeader';
-import { Part } from '../components/Part';
-import { SelectList } from '../components/SelectList';
-import { ButtonSuccess, ButtonWarning, LinkSuccess, LinkWarning, TermsOfService, WalletAction, WarningText } from '../components/styled';
-import { useAsyncAsHook } from '../hooks/useAsyncAsHook';
-import {
- acceptWithdrawal, getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, setExchangeTosAccepted, listExchanges, getExchangeTos
-} from "../wxApi";
-import { wxMain } from '../wxBackend.js';
-
-interface Props {
- talerWithdrawUri?: string;
-}
-
-export interface ViewProps {
- details: GetExchangeTosResult;
- withdrawalFee: AmountJson;
- exchangeBaseUrl: string;
- amount: AmountJson;
- onSwitchExchange: (ex: string) => void;
- onWithdraw: () => Promise<void>;
- onReview: (b: boolean) => void;
- onAccept: (b: boolean) => void;
- reviewing: boolean;
- reviewed: boolean;
- confirmed: boolean;
- terms: {
- value?: TermsDocument;
- status: TermsStatus;
- };
- knownExchanges: ExchangeListItem[];
-
-};
-
-type TermsStatus = 'new' | 'accepted' | 'changed' | 'notfound';
-
-type TermsDocument = TermsDocumentXml | TermsDocumentHtml | TermsDocumentPlain | TermsDocumentJson | TermsDocumentPdf;
-
-interface TermsDocumentXml {
- type: 'xml',
- document: Document,
-}
-
-interface TermsDocumentHtml {
- type: 'html',
- href: URL,
-}
-
-interface TermsDocumentPlain {
- type: 'plain',
- content: string,
-}
-
-interface TermsDocumentJson {
- type: 'json',
- data: any,
-}
-
-interface TermsDocumentPdf {
- type: 'pdf',
- location: URL,
-}
-
-function amountToString(text: AmountJson) {
- const aj = Amounts.jsonifyAmount(text)
- const amount = Amounts.stringifyValue(aj)
- return `${amount} ${aj.currency}`
-}
-
-export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges, amount, onWithdraw, onSwitchExchange, terms, reviewing, onReview, onAccept, reviewed, confirmed }: ViewProps) {
- const needsReview = terms.status === 'changed' || terms.status === 'new'
-
- const [switchingExchange, setSwitchingExchange] = useState<string | undefined>(undefined)
- const exchanges = knownExchanges.reduce((prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }), {})
-
- return (
- <WalletAction>
- <LogoHeader />
- <h2>
- {i18n.str`Digital cash withdrawal`}
- </h2>
- <section>
- <Part title="Total to withdraw" text={amountToString(Amounts.sub(amount, withdrawalFee).amount)} kind='positive' />
- <Part title="Chosen amount" text={amountToString(amount)} kind='neutral' />
- {Amounts.isNonZero(withdrawalFee) &&
- <Part title="Exchange fee" text={amountToString(withdrawalFee)} kind='negative' />
- }
- <Part title="Exchange" text={exchangeBaseUrl} kind='neutral' big />
- </section>
- {!reviewing &&
- <section>
- {switchingExchange !== undefined ? <Fragment>
- <div>
- <SelectList label="Known exchanges" list={exchanges} name="" onChange={onSwitchExchange} />
- </div>
- <LinkSuccess upperCased onClick={() => onSwitchExchange(switchingExchange)}>
- {i18n.str`Confirm exchange selection`}
- </LinkSuccess>
- </Fragment>
- : <LinkSuccess upperCased onClick={() => setSwitchingExchange("")}>
- {i18n.str`Switch exchange`}
- </LinkSuccess>}
-
- </section>
- }
- {!reviewing && reviewed &&
- <section>
- <LinkSuccess
- upperCased
- onClick={() => onReview(true)}
- >
- {i18n.str`Show terms of service`}
- </LinkSuccess>
- </section>
- }
- {terms.status === 'notfound' &&
- <section>
- <WarningText>
- {i18n.str`Exchange doesn't have terms of service`}
- </WarningText>
- </section>
- }
- {reviewing &&
- <section>
- {terms.status !== 'accepted' && terms.value && terms.value.type === 'xml' &&
- <TermsOfService>
- <ExchangeXmlTos doc={terms.value.document} />
- </TermsOfService>
- }
- {terms.status !== 'accepted' && terms.value && terms.value.type === 'plain' &&
- <div style={{ textAlign: 'left' }}>
- <pre>{terms.value.content}</pre>
- </div>
- }
- {terms.status !== 'accepted' && terms.value && terms.value.type === 'html' &&
- <iframe src={terms.value.href.toString()} />
- }
- {terms.status !== 'accepted' && terms.value && terms.value.type === 'pdf' &&
- <a href={terms.value.location.toString()} download="tos.pdf" >Download Terms of Service</a>
- }
- </section>}
- {reviewing && reviewed &&
- <section>
- <LinkSuccess
- upperCased
- onClick={() => onReview(false)}
- >
- {i18n.str`Hide terms of service`}
- </LinkSuccess>
- </section>
- }
- {(reviewing || reviewed) &&
- <section>
- <CheckboxOutlined
- name="terms"
- enabled={reviewed}
- label={i18n.str`I accept the exchange terms of service`}
- onToggle={() => {
- onAccept(!reviewed)
- onReview(false)
- }}
- />
- </section>
- }
-
- {/**
- * Main action section
- */}
- <section>
- {terms.status === 'new' && !reviewed && !reviewing &&
- <ButtonSuccess
- upperCased
- disabled={!exchangeBaseUrl}
- onClick={() => onReview(true)}
- >
- {i18n.str`Review exchange terms of service`}
- </ButtonSuccess>
- }
- {terms.status === 'changed' && !reviewed && !reviewing &&
- <ButtonWarning
- upperCased
- disabled={!exchangeBaseUrl}
- onClick={() => onReview(true)}
- >
- {i18n.str`Review new version of terms of service`}
- </ButtonWarning>
- }
- {(terms.status === 'accepted' || (needsReview && reviewed)) &&
- <ButtonSuccess
- upperCased
- disabled={!exchangeBaseUrl || confirmed}
- onClick={onWithdraw}
- >
- {i18n.str`Confirm withdrawal`}
- </ButtonSuccess>
- }
- {terms.status === 'notfound' &&
- <ButtonWarning
- upperCased
- disabled={!exchangeBaseUrl}
- onClick={onWithdraw}
- >
- {i18n.str`Withdraw anyway`}
- </ButtonWarning>
- }
- </section>
- </WalletAction>
- )
-}
-
-export function WithdrawPageWithParsedURI({ uri, uriInfo }: { uri: string, uriInfo: WithdrawUriInfoResponse }) {
- const [customExchange, setCustomExchange] = useState<string | undefined>(undefined)
- const [errorAccepting, setErrorAccepting] = useState<string | undefined>(undefined)
-
- const [reviewing, setReviewing] = useState<boolean>(false)
- const [reviewed, setReviewed] = useState<boolean>(false)
- const [confirmed, setConfirmed] = useState<boolean>(false)
-
- const knownExchangesHook = useAsyncAsHook(() => listExchanges())
-
- const knownExchanges = !knownExchangesHook || knownExchangesHook.hasError ? [] : knownExchangesHook.response.exchanges
- const withdrawAmount = Amounts.parseOrThrow(uriInfo.amount)
- const thisCurrencyExchanges = knownExchanges.filter(ex => ex.currency === withdrawAmount.currency)
-
- const exchange = customExchange || uriInfo.defaultExchangeBaseUrl || thisCurrencyExchanges[0]?.exchangeBaseUrl
- const detailsHook = useAsyncAsHook(async () => {
- if (!exchange) throw Error('no default exchange')
- const tos = await getExchangeTos(exchange, ['text/xml'])
- const info = await getExchangeWithdrawalInfo({
- exchangeBaseUrl: exchange,
- amount: withdrawAmount,
- tosAcceptedFormat: ['text/xml']
- })
- return { tos, info }
- })
-
- if (!detailsHook) {
- return <span><i18n.Translate>Getting withdrawal details.</i18n.Translate></span>;
- }
- if (detailsHook.hasError) {
- return <span><i18n.Translate>Problems getting details: {detailsHook.message}</i18n.Translate></span>;
- }
-
- const details = detailsHook.response
-
- const onAccept = async (): Promise<void> => {
- try {
- await setExchangeTosAccepted(exchange, details.tos.currentEtag)
- setReviewed(true)
- } catch (e) {
- if (e instanceof Error) {
- setErrorAccepting(e.message)
- }
- }
- }
-
- const onWithdraw = async (): Promise<void> => {
- setConfirmed(true)
- console.log("accepting exchange", exchange);
- try {
- const res = await acceptWithdrawal(uri, exchange);
- console.log("accept withdrawal response", res);
- if (res.confirmTransferUrl) {
- document.location.href = res.confirmTransferUrl;
- }
- } catch (e) {
- setConfirmed(false)
- }
- };
-
- const termsContent: TermsDocument | undefined = parseTermsOfServiceContent(details.tos.contentType, details.tos.content);
-
- const status: TermsStatus = !termsContent ? 'notfound' : (
- !details.tos.acceptedEtag ? 'new' : (
- details.tos.acceptedEtag !== details.tos.currentEtag ? 'changed' : 'accepted'
- ))
-
-
- return <View onWithdraw={onWithdraw}
- details={details.tos} amount={withdrawAmount}
- exchangeBaseUrl={exchange}
- withdrawalFee={details.info.withdrawFee} //FIXME
- terms={{
- status, value: termsContent
- }}
- onSwitchExchange={setCustomExchange}
- knownExchanges={knownExchanges}
- confirmed={confirmed}
- reviewed={reviewed} onAccept={onAccept}
- reviewing={reviewing} onReview={setReviewing}
- />
-}
-export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element {
- const uriInfoHook = useAsyncAsHook(() => !talerWithdrawUri ? Promise.reject(undefined) :
- getWithdrawalDetailsForUri({ talerWithdrawUri })
- )
-
- if (!talerWithdrawUri) {
- return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>;
- }
- if (!uriInfoHook) {
- return <span><i18n.Translate>Loading...</i18n.Translate></span>;
- }
- if (uriInfoHook.hasError) {
- return <span><i18n.Translate>This URI is not valid anymore: {uriInfoHook.message}</i18n.Translate></span>;
- }
- return <WithdrawPageWithParsedURI uri={talerWithdrawUri} uriInfo={uriInfoHook.response} />
-}
-
-function parseTermsOfServiceContent(type: string, text: string): TermsDocument | undefined {
- if (type === 'text/xml') {
- try {
- const document = new DOMParser().parseFromString(text, "text/xml")
- return { type: 'xml', document }
- } catch (e) {
- console.log(e)
- debugger;
- }
- } else if (type === 'text/html') {
- try {
- const href = new URL(text)
- return { type: 'html', href }
- } catch (e) {
- console.log(e)
- debugger;
- }
- } else if (type === 'text/json') {
- try {
- const data = JSON.parse(text)
- return { type: 'json', data }
- } catch (e) {
- console.log(e)
- debugger;
- }
- } else if (type === 'text/pdf') {
- try {
- const location = new URL(text)
- return { type: 'pdf', location }
- } catch (e) {
- console.log(e)
- debugger;
- }
- } else if (type === 'text/plain') {
- try {
- const content = text
- return { type: 'plain', content }
- } catch (e) {
- console.log(e)
- debugger;
- }
- }
- return undefined
-}
-
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
new file mode 100644
index 000000000..d33abffee
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
@@ -0,0 +1,139 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AmountJson,
+ AmountString,
+ CurrencySpecification,
+ ExchangeListItem
+} from "@gnu-taler/taler-util";
+import { Loading } from "../../components/Loading.js";
+import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
+import {
+ AmountFieldHandler,
+ ButtonHandler,
+ SelectFieldHandler,
+} from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
+import {
+ useComponentStateFromParams,
+ useComponentStateFromURI,
+} from "./state.js";
+
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { ExchangeSelectionPage } from "../../wallet/ExchangeSelection/index.js";
+import { NoExchangesView } from "../../wallet/ExchangeSelection/views.js";
+import { FinalStateOperation, SelectAmountView, SuccessView } from "./views.js";
+
+export interface PropsFromURI {
+ talerWithdrawUri: string | undefined;
+ cancel: () => Promise<void>;
+ onSuccess: (txid: string) => Promise<void>;
+}
+
+export interface PropsFromParams {
+ talerExchangeWithdrawUri: string | undefined;
+ amount: string | undefined;
+ cancel: () => Promise<void>;
+ onSuccess: (txid: string) => Promise<void>;
+ onAmountChanged: (amount: AmountString) => Promise<void>;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | SelectExchangeState.NoExchangeFound
+ | SelectExchangeState.Selecting
+ | State.SelectAmount
+ | State.AlreadyCompleted
+ | State.Success;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface SelectAmount {
+ status: "select-amount";
+ error: undefined;
+ exchangeBaseUrl: string;
+ confirm: ButtonHandler;
+ amount: AmountFieldHandler;
+ currency: string;
+ }
+ export interface AlreadyCompleted {
+ status: "already-completed";
+ operationState: "confirmed" | "aborted" | "selected";
+ thisWallet: boolean;
+ redirectToTx: () => void;
+ confirmTransferUrl?: string,
+ error: undefined;
+ }
+
+ export type Success = {
+ status: "success";
+ error: undefined;
+
+ currentExchange: ExchangeListItem;
+
+ chosenAmount: AmountJson;
+ withdrawalFee: AmountJson;
+ toBeReceived: AmountJson;
+
+ doWithdrawal: ButtonHandler;
+ doSelectExchange: ButtonHandler;
+
+ chooseCurrencies: string[];
+ selectedCurrency: string;
+ changeCurrency: (s: string) => void;
+ conversionInfo: {
+ spec: CurrencySpecification,
+ amount: AmountJson,
+ } | undefined;
+
+ ageRestriction?: SelectFieldHandler;
+
+ talerWithdrawUri?: string;
+ cancel: () => Promise<void>;
+ };
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ "select-amount": SelectAmountView,
+ "no-exchange-found": NoExchangesView,
+ "selecting-exchange": ExchangeSelectionPage,
+ success: SuccessView,
+ "already-completed": FinalStateOperation,
+};
+
+export const WithdrawPageFromURI = compose(
+ "WithdrawPageFromURI_Withdraw",
+ (p: PropsFromURI) => useComponentStateFromURI(p),
+ viewMapping,
+);
+export const WithdrawPageFromParams = compose(
+ "WithdrawPageFromParams",
+ (p: PropsFromParams) => useComponentStateFromParams(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
new file mode 100644
index 000000000..f592072ff
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -0,0 +1,522 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AmountJson,
+ AmountString,
+ Amounts,
+ ExchangeFullDetails,
+ ExchangeListItem,
+ NotificationType,
+ TransactionMajorState,
+ parseWithdrawExchangeUri,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
+import { RecursiveState } from "../../utils/index.js";
+import { PropsFromParams, PropsFromURI, State } from "./index.js";
+
+export function useComponentStateFromParams({
+ talerExchangeWithdrawUri: maybeTalerUri,
+ amount,
+ cancel,
+ onAmountChanged,
+ onSuccess,
+}: PropsFromParams): RecursiveState<State> {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const paramsAmount = amount ? Amounts.parse(amount) : undefined;
+ const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>();
+ const uriInfoHook = useAsyncAsHook(async () => {
+ const exchanges = await api.wallet.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+ const uri = maybeTalerUri
+ ? parseWithdrawExchangeUri(maybeTalerUri)
+ : undefined;
+ const exchangeByTalerUri = updatedExchangeByUser ?? uri?.exchangeBaseUrl;
+
+ let ex: ExchangeFullDetails | undefined;
+ if (exchangeByTalerUri) {
+ await api.wallet.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchangeByTalerUri,
+ });
+ const info = await api.wallet.call(
+ WalletApiOperation.GetExchangeDetailedInfo,
+ {
+ exchangeBaseUrl: exchangeByTalerUri,
+ },
+ );
+
+ ex = info.exchange;
+ }
+ const chosenAmount =
+ !uri || !uri.amount ? undefined : Amounts.parse(uri.amount);
+ return { amount: chosenAmount, exchanges, exchange: ex };
+ });
+
+ if (!uriInfoHook) return { status: "loading", error: undefined };
+
+ if (uriInfoHook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the list of exchanges`,
+ uriInfoHook,
+ ),
+ };
+ }
+
+ useEffect(() => {
+ uriInfoHook?.retry();
+ }, [amount]);
+
+ const exchangeByTalerUri = uriInfoHook.response.exchange?.exchangeBaseUrl;
+ const exchangeList = uriInfoHook.response.exchanges.exchanges;
+
+ const maybeAmount = uriInfoHook.response.amount ?? paramsAmount;
+
+ if (!maybeAmount) {
+ const exchangeBaseUrl =
+ uriInfoHook.response.exchange?.exchangeBaseUrl ??
+ (exchangeList.length > 0 ? exchangeList[0].exchangeBaseUrl : undefined);
+ const currency =
+ uriInfoHook.response.exchange?.currency ??
+ (exchangeList.length > 0 ? exchangeList[0].currency : undefined);
+
+ if (!exchangeBaseUrl) {
+ return {
+ status: "error",
+ error: {
+ message: i18n.str`Can't withdraw from exchange`,
+ description: i18n.str`Missing base URL`,
+ cause: undefined,
+ context: {},
+ type: "error",
+ },
+ };
+ }
+ if (!currency) {
+ return {
+ status: "error",
+ error: {
+ message: i18n.str`Can't withdraw from exchange`,
+ description: i18n.str`Missing unknown currency`,
+ cause: undefined,
+ context: {},
+ type: "error",
+ },
+ };
+ }
+ return () => {
+ const { pushAlertOnError } = useAlertContext();
+ const [amount, setAmount] = useState<AmountJson>(
+ Amounts.zeroOfCurrency(currency),
+ );
+ const isValid = Amounts.isNonZero(amount);
+ return {
+ status: "select-amount",
+ currency,
+ exchangeBaseUrl,
+ error: undefined,
+ confirm: {
+ onClick: isValid
+ ? pushAlertOnError(async () => {
+ onAmountChanged(Amounts.stringify(amount));
+ })
+ : undefined,
+ },
+ amount: {
+ value: amount,
+ onInput: pushAlertOnError(async (e) => {
+ setAmount(e);
+ }),
+ },
+ };
+ };
+ }
+ const chosenAmount = maybeAmount;
+
+ async function doManualWithdraw(
+ exchange: string,
+ ageRestricted: number | undefined,
+ amount: AmountString,
+ ): Promise<{
+ transactionId: string;
+ confirmTransferUrl: string | undefined;
+ }> {
+ const res = await api.wallet.call(
+ WalletApiOperation.AcceptManualWithdrawal,
+ {
+ exchangeBaseUrl: exchange,
+ amount,
+ restrictAge: ageRestricted,
+ },
+ );
+ return {
+ confirmTransferUrl: undefined,
+ transactionId: res.transactionId,
+ };
+ }
+
+ return () =>
+ exchangeSelectionState(
+ doManualWithdraw,
+ cancel,
+ onSuccess,
+ undefined,
+ chosenAmount,
+ exchangeList,
+ exchangeByTalerUri,
+ setUpdatedExchangeByUser,
+ );
+}
+
+export function useComponentStateFromURI({
+ talerWithdrawUri: maybeTalerUri,
+ cancel,
+ onSuccess,
+}: PropsFromURI): RecursiveState<State> {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+
+ const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>();
+ /**
+ * Ask the wallet about the withdraw URI
+ */
+ const uriInfoHook = useAsyncAsHook(async () => {
+ if (!maybeTalerUri) throw Error("ERROR_NO-URI-FOR-WITHDRAWAL");
+ const talerWithdrawUri = maybeTalerUri.startsWith("ext+")
+ ? maybeTalerUri.substring(4)
+ : maybeTalerUri;
+
+ const uriInfo = await api.wallet.call(
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
+ {
+ talerWithdrawUri,
+ selectedExchange: updatedExchangeByUser,
+ },
+ );
+ const {
+ amount,
+ defaultExchangeBaseUrl,
+ possibleExchanges,
+ confirmTransferUrl,
+ status,
+ } = uriInfo.info;
+ const txInfo =
+ uriInfo.transactionId === undefined
+ ? undefined
+ : await api.wallet.call(WalletApiOperation.GetTransactionById, {
+ transactionId: uriInfo.transactionId,
+ });
+ return {
+ talerWithdrawUri,
+ status,
+ transactionId: uriInfo.transactionId,
+ txInfo: txInfo,
+ confirmTransferUrl,
+ amount: Amounts.parseOrThrow(amount),
+ thisExchange: defaultExchangeBaseUrl,
+ exchanges: possibleExchanges,
+ };
+ });
+
+ const readyToListen = uriInfoHook && !uriInfoHook.hasError;
+
+ useEffect(() => {
+ if (!uriInfoHook || uriInfoHook.hasError) {
+ return;
+ }
+ const txId = uriInfoHook.response.transactionId;
+
+ return api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
+ (notif) => {
+ if (
+ notif.type === NotificationType.TransactionStateTransition &&
+ notif.transactionId === txId
+ ) {
+ uriInfoHook.retry();
+ }
+ },
+ );
+ }, [readyToListen]);
+
+ if (!uriInfoHook) return { status: "loading", error: undefined };
+
+ if (uriInfoHook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load info from URI`,
+ uriInfoHook,
+ ),
+ };
+ }
+
+ const uri = uriInfoHook.response.talerWithdrawUri;
+ const txId = uriInfoHook.response.transactionId;
+ const chosenAmount = uriInfoHook.response.amount;
+ const defaultExchange = uriInfoHook.response.thisExchange;
+ const exchangeList = uriInfoHook.response.exchanges;
+
+ async function doManagedWithdraw(
+ exchange: string,
+ ageRestricted: number | undefined,
+ amount: AmountString,
+ ): Promise<{
+ transactionId: string;
+ confirmTransferUrl: string | undefined;
+ }> {
+ if (!txId) {
+ throw Error("can't confirm transaction");
+ }
+ const res = await api.wallet.call(WalletApiOperation.ConfirmWithdrawal, {
+ exchangeBaseUrl: exchange,
+ amount,
+ restrictAge: ageRestricted,
+ transactionId: txId,
+ });
+ return {
+ confirmTransferUrl: res.confirmTransferUrl,
+ transactionId: res.transactionId,
+ };
+ }
+
+ if (uriInfoHook.response.txInfo && uriInfoHook.response.status !== "pending") {
+ const info = uriInfoHook.response.txInfo;
+ return {
+ status: "already-completed",
+ operationState: uriInfoHook.response.status,
+ confirmTransferUrl: uriInfoHook.response.confirmTransferUrl,
+ thisWallet: info.txState.major === TransactionMajorState.Pending,
+ redirectToTx: () => onSuccess(info.transactionId),
+ error: undefined,
+ };
+ }
+
+ return useCallback(() => {
+ return exchangeSelectionState(
+ doManagedWithdraw,
+ cancel,
+ onSuccess,
+ uri,
+ chosenAmount,
+ exchangeList,
+ defaultExchange,
+ setUpdatedExchangeByUser,
+ );
+ }, []);
+}
+
+type ManualOrManagedWithdrawFunction = (
+ exchange: string,
+ ageRestricted: number | undefined,
+ amount: AmountString,
+) => Promise<{ transactionId: string; confirmTransferUrl: string | undefined }>;
+
+function exchangeSelectionState(
+ doWithdraw: ManualOrManagedWithdrawFunction,
+ cancel: () => Promise<void>,
+ onSuccess: (txid: string) => Promise<void>,
+ talerWithdrawUri: string | undefined,
+ chosenAmount: AmountJson,
+ exchangeList: ExchangeListItem[],
+ exchangeSuggestedByTheBank: string | undefined,
+ onExchangeUpdated: (ex: string) => void,
+): RecursiveState<State> {
+ const api = useBackendContext();
+ const selectedExchange = useSelectedExchange({
+ currency: chosenAmount.currency,
+ defaultExchange: exchangeSuggestedByTheBank,
+ list: exchangeList,
+ });
+
+ const current =
+ selectedExchange.status !== "ready"
+ ? undefined
+ : selectedExchange.selected.exchangeBaseUrl;
+ useEffect(() => {
+ if (current) {
+ onExchangeUpdated(current);
+ }
+ }, [current]);
+
+ if (selectedExchange.status !== "ready") {
+ return selectedExchange;
+ }
+
+ return useCallback(():
+ | State.Success
+ | State.LoadingUriError
+ | State.Loading => {
+ const { i18n } = useTranslationContext();
+ const { pushAlertOnError } = useAlertContext();
+ const [ageRestricted, setAgeRestricted] = useState(0);
+ const currentExchange = selectedExchange.selected;
+
+ const [selectedCurrency, setSelectedCurrency] = useState<string>(
+ chosenAmount.currency,
+ );
+ /**
+ * With the exchange and amount, ask the wallet the information
+ * about the withdrawal
+ */
+ const amountHook = useAsyncAsHook(async () => {
+ const info = await api.wallet.call(
+ WalletApiOperation.GetWithdrawalDetailsForAmount,
+ {
+ exchangeBaseUrl: currentExchange.exchangeBaseUrl,
+ amount: Amounts.stringify(chosenAmount),
+ restrictAge: ageRestricted,
+ },
+ );
+
+ const withdrawAmount = {
+ raw: Amounts.parseOrThrow(info.amountRaw),
+ effective: Amounts.parseOrThrow(info.amountEffective),
+ };
+
+ return {
+ amount: withdrawAmount,
+ ageRestrictionOptions: info.ageRestrictionOptions,
+ accounts: info.withdrawalAccountsList,
+ };
+ }, []);
+
+ const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
+
+ async function doWithdrawAndCheckError(): Promise<void> {
+ try {
+ setDoingWithdraw(true);
+ const res = await doWithdraw(
+ currentExchange.exchangeBaseUrl,
+ !ageRestricted ? undefined : ageRestricted,
+ Amounts.stringify(chosenAmount),
+ );
+ if (res.confirmTransferUrl) {
+ document.location.href = res.confirmTransferUrl;
+ } else {
+ onSuccess(res.transactionId);
+ }
+ } catch (e) {
+ console.error(e);
+ // if (e instanceof TalerError) {
+ // }
+ }
+ setDoingWithdraw(false);
+ }
+
+ if (!amountHook) {
+ return { status: "loading", error: undefined };
+ }
+ if (amountHook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load the withdrawal details`,
+ amountHook,
+ ),
+ };
+ }
+ if (!amountHook.response) {
+ return { status: "loading", error: undefined };
+ }
+
+ const withdrawalFee = Amounts.sub(
+ amountHook.response.amount.raw,
+ amountHook.response.amount.effective,
+ ).amount;
+ const toBeReceived = amountHook.response.amount.effective;
+
+ const ageRestrictionOptions =
+ amountHook.response.ageRestrictionOptions?.reduce(
+ (p, c) => ({ ...p, [c]: `under ${c}` }),
+ {} as Record<string, string>,
+ );
+
+ const ageRestrictionEnabled = ageRestrictionOptions !== undefined;
+ if (ageRestrictionEnabled) {
+ ageRestrictionOptions["0"] = "Not restricted";
+ }
+
+ //TODO: calculate based on exchange info
+ const ageRestriction = ageRestrictionEnabled
+ ? {
+ list: ageRestrictionOptions,
+ value: String(ageRestricted),
+ onChange: pushAlertOnError(async (v: string) =>
+ setAgeRestricted(parseInt(v, 10)),
+ ),
+ }
+ : undefined;
+
+ const altCurrencies = amountHook.response.accounts
+ .filter((a) => !!a.currencySpecification)
+ .map((a) => a.currencySpecification!.name);
+ const chooseCurrencies =
+ altCurrencies.length === 0
+ ? []
+ : [toBeReceived.currency, ...altCurrencies];
+
+ const convAccount = amountHook.response.accounts.find((c) => {
+ return (
+ c.currencySpecification &&
+ c.currencySpecification.name === selectedCurrency
+ );
+ });
+ const conversionInfo = !convAccount
+ ? undefined
+ : {
+ spec: convAccount.currencySpecification!,
+ amount: Amounts.parseOrThrow(convAccount.transferAmount!),
+ };
+
+ return {
+ status: "success",
+ error: undefined,
+ doSelectExchange: selectedExchange.doSelect,
+ currentExchange,
+ toBeReceived,
+ chooseCurrencies,
+ selectedCurrency,
+ changeCurrency: (s) => {
+ setSelectedCurrency(s);
+ },
+ conversionInfo,
+ withdrawalFee,
+ chosenAmount,
+ talerWithdrawUri,
+ ageRestriction,
+ doWithdrawal: {
+ onClick: doingWithdraw
+ ? undefined
+ : pushAlertOnError(doWithdrawAndCheckError),
+ },
+ cancel,
+ };
+ }, []);
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
new file mode 100644
index 000000000..29f39054f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
@@ -0,0 +1,327 @@
+/*
+ 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 { CurrencySpecification, ExchangeListItem } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
+// import { TermsState } from "../../utils/index.js";
+import { SuccessView, FinalStateOperation } from "./views.js";
+
+export default {
+ title: "withdraw",
+};
+
+const ageRestrictionOptions: Record<string, string> = "6:12:18"
+ .split(":")
+ .reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {});
+
+ageRestrictionOptions["0"] = "Not restricted";
+
+const ageRestrictionSelectField = {
+ list: ageRestrictionOptions,
+ value: "0",
+};
+
+export const TermsOfServiceNotYetLoaded = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
+ doWithdrawal: { onClick: nullFunction },
+ currentExchange: {
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ tos: {},
+ } as Partial<ExchangeListItem> as any,
+ withdrawalFee: {
+ currency: "USD",
+ fraction: 10000000,
+ value: 1,
+ },
+ doSelectExchange: {},
+ toBeReceived: {
+ currency: "USD",
+ fraction: 0,
+ value: 1,
+ },
+ chooseCurrencies: [],
+});
+
+export const AlreadyAborted = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "aborted"
+});
+export const AlreadySelected = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "selected"
+});
+export const AlreadyConfirmed = tests.createExample(FinalStateOperation, {
+ error: undefined,
+ status: "already-completed",
+ operationState: "confirmed"
+});
+
+
+export const WithSomeFee = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
+ doWithdrawal: { onClick: nullFunction },
+ currentExchange: {
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ tos: {},
+ } as Partial<ExchangeListItem> as any,
+ withdrawalFee: {
+ currency: "USD",
+ fraction: 10000000,
+ value: 1,
+ },
+ toBeReceived: {
+ currency: "USD",
+ fraction: 0,
+ value: 1,
+ },
+ doSelectExchange: {},
+ chooseCurrencies: [],
+});
+
+export const WithoutFee = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "USD",
+ value: 2,
+ fraction: 0,
+ },
+ doWithdrawal: { onClick: nullFunction },
+ currentExchange: {
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ tos: {},
+ } as Partial<ExchangeListItem> as any,
+ withdrawalFee: {
+ currency: "USD",
+ fraction: 0,
+ value: 0,
+ },
+ doSelectExchange: {},
+ toBeReceived: {
+ currency: "USD",
+ fraction: 0,
+ value: 2,
+ },
+ chooseCurrencies: [],
+});
+
+export const EditExchangeUntouched = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
+ doWithdrawal: { onClick: nullFunction },
+ currentExchange: {
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ tos: {},
+ } as Partial<ExchangeListItem> as any,
+ withdrawalFee: {
+ currency: "USD",
+ fraction: 0,
+ value: 0,
+ },
+ doSelectExchange: {},
+ toBeReceived: {
+ currency: "USD",
+ fraction: 0,
+ value: 2,
+ },
+ chooseCurrencies: [],
+});
+
+export const EditExchangeModified = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
+ doWithdrawal: { onClick: nullFunction },
+ currentExchange: {
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ tos: {},
+ } as Partial<ExchangeListItem> as any,
+ withdrawalFee: {
+ currency: "USD",
+ fraction: 0,
+ value: 0,
+ },
+ doSelectExchange: {},
+ toBeReceived: {
+ currency: "USD",
+ fraction: 0,
+ value: 2,
+ },
+ chooseCurrencies: [],
+});
+
+export const WithAgeRestriction = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ ageRestriction: ageRestrictionSelectField,
+ chosenAmount: {
+ currency: "USD",
+ value: 2,
+ fraction: 10000000,
+ },
+ doSelectExchange: {},
+ doWithdrawal: { onClick: nullFunction },
+ currentExchange: {
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ tos: {},
+ } as Partial<ExchangeListItem> as any,
+ withdrawalFee: {
+ currency: "USD",
+ fraction: 0,
+ value: 0,
+ },
+ toBeReceived: {
+ currency: "USD",
+ fraction: 0,
+ value: 2,
+ },
+ chooseCurrencies: [],
+});
+
+export const WithAlternateCurrenciesNETZBON = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
+ chooseCurrencies: ["NETZBON", "EUR"],
+ selectedCurrency: "NETZBON",
+ doWithdrawal: { onClick: nullFunction },
+ currentExchange: {
+ exchangeBaseUrl: "https://exchange.netzbon.ch",
+ tos: {},
+ } as Partial<ExchangeListItem> as any,
+ withdrawalFee: {
+ currency: "NETZBON",
+ fraction: 10000000,
+ value: 1,
+ },
+ doSelectExchange: {},
+ toBeReceived: {
+ currency: "NETZBON",
+ fraction: 0,
+ value: 1,
+ },
+});
+
+export const WithAlternateCurrenciesEURO = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
+ chooseCurrencies: ["NETZBON", "EUR"],
+ selectedCurrency: "EUR",
+ changeCurrency: () => { },
+ conversionInfo: {
+ spec: {
+ name: "EUR"
+ } as CurrencySpecification,
+ amount: {
+ currency: "EUR",
+ fraction: 10000000,
+ value: 1,
+ }
+ },
+ doWithdrawal: { onClick: nullFunction },
+ currentExchange: {
+ exchangeBaseUrl: "https://exchange.netzbon.ch",
+ tos: {},
+ } as Partial<ExchangeListItem> as any,
+ withdrawalFee: {
+ currency: "NETZBON",
+ fraction: 10000000,
+ value: 1,
+ },
+ doSelectExchange: {},
+ toBeReceived: {
+ currency: "NETZBON",
+ fraction: 0,
+ value: 1,
+ },
+});
+
+export const WithAlternateCurrenciesEURO11 = tests.createExample(SuccessView, {
+ error: undefined,
+ status: "success",
+ chosenAmount: {
+ currency: "NETZBON",
+ value: 2,
+ fraction: 10000000,
+ },
+ chooseCurrencies: ["NETZBON", "EUR"],
+ selectedCurrency: "EUR",
+ changeCurrency: () => { },
+ conversionInfo: {
+ spec: {
+ name: "EUR"
+ } as CurrencySpecification,
+ amount: {
+ currency: "EUR",
+ fraction: 10000000,
+ value: 2,
+ }
+ },
+ doWithdrawal: { onClick: nullFunction },
+ currentExchange: {
+ exchangeBaseUrl: "https://exchange.netzbon.ch",
+ tos: {},
+ } as Partial<ExchangeListItem> as any,
+ withdrawalFee: {
+ currency: "NETZBON",
+ fraction: 10000000,
+ value: 1,
+ },
+ doSelectExchange: {},
+ toBeReceived: {
+ currency: "NETZBON",
+ fraction: 0,
+ value: 1,
+ },
+});
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
new file mode 100644
index 000000000..860cf1099
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -0,0 +1,296 @@
+/*
+ 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 {
+ AmountString,
+ Amounts,
+ ExchangeEntryStatus,
+ ExchangeListItem,
+ ExchangeTosStatus,
+ ScopeType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { expect } from "chai";
+import * as tests from "@gnu-taler/web-util/testing";
+import { createWalletApiMock } from "../../test-utils.js";
+import { useComponentStateFromURI } from "./state.js";
+
+const exchanges: ExchangeListItem[] = [
+ {
+ currency: "ARS",
+ exchangeBaseUrl: "http://exchange.demo.taler.net",
+ paytoUris: [],
+ tosStatus: ExchangeTosStatus.Accepted,
+ exchangeStatus: ExchangeEntryStatus.Used,
+ permanent: true,
+ auditors: [
+ {
+ auditor_pub: "pubpubpubpubpub",
+ auditor_url: "https://audotor.taler.net",
+ denomination_keys: [],
+ },
+ ],
+ denomFees: {
+ deposit: [],
+ refresh: [],
+ refund: [],
+ withdraw: [],
+ },
+ globalFees: [],
+ transferFees: {},
+ wireInfo: {
+ accounts: [],
+ feesForType: {},
+ },
+ } as Partial<ExchangeListItem> as ExchangeListItem,
+];
+
+const nullFunction = async (): Promise<void> => {
+ null;
+};
+
+describe("Withdraw CTA states", () => {
+ it("should tell the user that the URI is missing", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ const props = {
+ talerWithdrawUri: undefined,
+ cancel: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentStateFromURI,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ ({ status, error }) => {
+ if (status != "error") expect.fail();
+ if (!error) expect.fail();
+ // if (!error.hasError) expect.fail();
+ // if (error.operational) expect.fail();
+ expect(error.description).eq("ERROR_NO-URI-FOR-WITHDRAWAL");
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it.skip("should tell the user that there is not known exchange", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerWithdrawUri: "taler-withdraw://",
+ cancel: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
+ undefined,
+ {
+ transactionId: "123",
+ info: {
+ status: "pending",
+ operationId: "123",
+ amount: "EUR:2" as AmountString,
+ possibleExchanges: [],
+ }
+ },
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentStateFromURI,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ ({ status, error }) => {
+ expect(status).equals("no-exchange-found");
+ expect(error).undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it.skip("should be able to withdraw if tos are ok", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerWithdrawUri: "taler-withdraw://",
+ cancel: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PrepareBankIntegratedWithdrawal,
+ undefined,
+ {
+ transactionId: "123",
+ info: {
+ status: "pending",
+ operationId: "123",
+ amount: "ARS:2" as AmountString,
+ possibleExchanges: exchanges,
+ defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
+ }
+ },
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetWithdrawalDetailsForAmount,
+ undefined,
+ {
+ amountRaw: "ARS:2" as AmountString,
+ amountEffective: "ARS:2" as AmountString,
+ paytoUris: ["payto://"],
+ tosAccepted: true,
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://asd"
+ },
+ withdrawalAccountsList: [],
+ ageRestrictionOptions: [],
+ numCoins: 42,
+ },
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentStateFromURI,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ expect(state.status).equals("success");
+ if (state.status !== "success") return;
+
+ expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
+ expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
+ expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
+
+ expect(state.doWithdrawal.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it.skip("should accept the tos before withdraw", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = {
+ talerWithdrawUri: "taler-withdraw://",
+ cancel: nullFunction,
+ onSuccess: nullFunction,
+ };
+
+ const exchangeWithNewTos = exchanges.map((e) => ({
+ ...e,
+ tosStatus: ExchangeTosStatus.Proposed,
+ }));
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ undefined,
+ {
+ status: "pending",
+ operationId: "123",
+ amount: "ARS:2" as AmountString,
+ possibleExchanges: exchangeWithNewTos,
+ defaultExchangeBaseUrl: exchangeWithNewTos[0].exchangeBaseUrl,
+ },
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetWithdrawalDetailsForAmount,
+ undefined,
+ {
+ amountRaw: "ARS:2" as AmountString,
+ amountEffective: "ARS:2" as AmountString,
+ paytoUris: ["payto://"],
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://asd"
+ },
+ tosAccepted: false,
+ withdrawalAccountsList: [],
+ ageRestrictionOptions: [],
+ numCoins: 42,
+ },
+ );
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ undefined,
+ {
+ status: "pending",
+ operationId: "123",
+ amount: "ARS:2" as AmountString,
+ possibleExchanges: exchanges,
+ defaultExchangeBaseUrl: exchanges[0].exchangeBaseUrl,
+ },
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentStateFromURI,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equals("loading");
+ },
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ (state) => {
+ expect(state.status).equals("success");
+ if (state.status !== "success") return;
+
+ expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
+ expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
+ expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
+
+ expect(state.doWithdrawal.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
new file mode 100644
index 000000000..cdddd9bbc
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
@@ -0,0 +1,333 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Amount } from "../../components/Amount.js";
+import { AmountField } from "../../components/AmountField.js";
+import { Part } from "../../components/Part.js";
+import { QR } from "../../components/QR.js";
+import { SelectList } from "../../components/SelectList.js";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
+import {
+ Input,
+ LinkSuccess,
+ SvgIcon,
+ WarningBox,
+} from "../../components/styled/index.js";
+import { Button } from "../../mui/Button.js";
+import { Grid } from "../../mui/Grid.js";
+import editIcon from "../../svg/edit_24px.inline.svg";
+import {
+ ExchangeDetails,
+ WithdrawDetails,
+ getAmountWithFee,
+} from "../../wallet/Transaction.js";
+import { State } from "./index.js";
+import { EnabledBySettings } from "../../components/EnabledBySettings.js";
+
+export function FinalStateOperation(state: State.AlreadyCompleted): VNode {
+ const { i18n } = useTranslationContext();
+ // document.location.href = res.confirmTransferUrl
+ if (state.thisWallet) {
+ switch (state.operationState) {
+ case "confirmed": {
+ state.redirectToTx();
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has already been completed.
+ </i18n.Translate>
+ </div>
+ </WarningBox>
+ );
+ }
+ case "aborted": {
+ state.redirectToTx();
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has already been aborted
+ </i18n.Translate>
+ </div>
+ </WarningBox>
+ );
+ }
+ case "selected": {
+ if (state.confirmTransferUrl) {
+ document.location.href = state.confirmTransferUrl;
+ }
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has started and should be completed in the bank.
+ </i18n.Translate>
+ </div>
+ {state.confirmTransferUrl && (
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ You can confirm the operation in
+ </i18n.Translate>
+ &nbsp;
+ <a
+ target="_bank"
+ rel="noreferrer"
+ href={state.confirmTransferUrl}
+ >
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </div>
+ )}
+ </WarningBox>
+ );
+ }
+ }
+ }
+
+ switch (state.operationState) {
+ case "confirmed":
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has already been completed by another wallet.
+ </i18n.Translate>
+ </div>
+ </WarningBox>
+ );
+ case "aborted":
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has already been aborted
+ </i18n.Translate>
+ </div>
+ </WarningBox>
+ );
+ case "selected":
+ return (
+ <WarningBox>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>
+ This operation has already been used by another wallet.
+ </i18n.Translate>
+ </div>
+ <div style={{ justifyContent: "center", lineHeight: "25px" }}>
+ <i18n.Translate>It can be confirmed in</i18n.Translate>&nbsp;
+ <a target="_bank" rel="noreferrer" href={state.confirmTransferUrl}>
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </div>
+ </WarningBox>
+ );
+ }
+}
+
+export function SuccessView(state: State.Success): VNode {
+ const { i18n } = useTranslationContext();
+ // const currentTosVersionIsAccepted =
+ // state.currentExchange.tosStatus === ExchangeTosStatus.Accepted;
+ return (
+ <Fragment>
+ <section style={{ textAlign: "left" }}>
+ <Part
+ title={
+ <div
+ style={{
+ display: "flex",
+ alignItems: "center",
+ }}
+ >
+ <i18n.Translate>Exchange</i18n.Translate>
+ <EnabledBySettings name="showExchangeManagement">
+ <Button onClick={state.doSelectExchange.onClick} variant="text">
+ <SvgIcon
+ title="Edit"
+ dangerouslySetInnerHTML={{ __html: editIcon }}
+ color="black"
+ />
+ </Button>
+ </EnabledBySettings>
+ </div>
+ }
+ text={
+ <ExchangeDetails exchange={state.currentExchange.exchangeBaseUrl} />
+ }
+ kind="neutral"
+ big
+ />
+ {state.chooseCurrencies.length > 0 ? (
+ <Fragment>
+ <p>
+ {state.chooseCurrencies.map((currency) => {
+ return (
+ <Button
+ key={currency}
+ variant={
+ currency === state.selectedCurrency
+ ? "contained"
+ : "outlined"
+ }
+ onClick={async () => {
+ state.changeCurrency(currency);
+ }}
+ >
+ {currency}
+ </Button>
+ );
+ })}
+ </p>
+ </Fragment>
+ ) : (
+ <Fragment />
+ )}
+
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <WithdrawDetails
+ conversion={state.conversionInfo?.amount}
+ amount={getAmountWithFee(
+ state.toBeReceived,
+ state.chosenAmount,
+ "credit",
+ )}
+ />
+ }
+ />
+ {state.ageRestriction && (
+ <Input>
+ <SelectList
+ label={i18n.str`Age restriction`}
+ list={state.ageRestriction.list}
+ name="age"
+ value={state.ageRestriction.value}
+ onChange={state.ageRestriction.onChange}
+ />
+ </Input>
+ )}
+ </section>
+
+ <section>
+ {/* <div> */}
+ <TermsOfService exchangeUrl={state.currentExchange.exchangeBaseUrl}>
+ <Button
+ variant="contained"
+ color="success"
+ disabled={!state.doWithdrawal.onClick}
+ onClick={state.doWithdrawal.onClick}
+ >
+ <i18n.Translate>
+ Withdraw &nbsp; <Amount value={state.toBeReceived} />
+ </i18n.Translate>
+ </Button>
+ </TermsOfService>
+ {/* </div>
+ <div style={{ marginTop: 20 }}>
+ <Button
+ variant="text"
+ color="success"
+
+ disabled={!state.doAbort.onClick}
+ onClick={state.doAbort.onClick}
+ >
+ <i18n.Translate>
+ Cancel
+ </i18n.Translate>
+ </Button>
+ </div> */}
+ </section>
+ {state.talerWithdrawUri ? (
+ <WithdrawWithMobile talerWithdrawUri={state.talerWithdrawUri} />
+ ) : undefined}
+ </Fragment>
+ );
+}
+
+function WithdrawWithMobile({
+ talerWithdrawUri,
+}: {
+ talerWithdrawUri: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [showQR, setShowQR] = useState<boolean>(false);
+
+ return (
+ <section>
+ <LinkSuccess upperCased onClick={() => setShowQR((qr) => !qr)}>
+ {!showQR ? i18n.str`Withdraw to a mobile phone` : i18n.str`Hide QR`}
+ </LinkSuccess>
+ {showQR && (
+ <div>
+ <QR text={talerWithdrawUri} />
+ <i18n.Translate>
+ Scan the QR code or &nbsp;
+ <a href={talerWithdrawUri}>
+ <i18n.Translate>click here</i18n.Translate>
+ </a>
+ </i18n.Translate>
+ </div>
+ )}
+ </section>
+ );
+}
+
+export function SelectAmountView({
+ amount,
+ exchangeBaseUrl,
+ confirm,
+}: State.SelectAmount): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <section style={{ textAlign: "left" }}>
+ <Part
+ title={
+ <div
+ style={{
+ display: "flex",
+ alignItems: "center",
+ }}
+ >
+ <i18n.Translate>Exchange</i18n.Translate>
+ </div>
+ }
+ text={<ExchangeDetails exchange={exchangeBaseUrl} />}
+ kind="neutral"
+ big
+ />
+ <Grid container columns={2} justifyContent="space-between">
+ <AmountField label={i18n.str`Amount`} required handler={amount} />
+ </Grid>
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="info"
+ disabled={!confirm.onClick}
+ onClick={confirm.onClick}
+ >
+ <i18n.Translate>See details</i18n.Translate>
+ </Button>
+ </section>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/index.stories.ts b/packages/taler-wallet-webextension/src/cta/index.stories.ts
new file mode 100644
index 000000000..36e9cd1b9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/index.stories.ts
@@ -0,0 +1,29 @@
+/*
+ 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)
+ */
+
+export * as a1 from "./Deposit/stories.jsx";
+export * as a3 from "./Payment/stories.jsx";
+export * as a4 from "./Refund/stories.jsx";
+export * as a6 from "./Withdraw/stories.jsx";
+export * as a8 from "./InvoiceCreate/stories.js";
+export * as a9 from "./InvoicePay/stories.js";
+export * as a10 from "./TransferCreate/stories.js";
+export * as a11 from "./TransferPickup/stories.js";
diff --git a/packages/taler-wallet-webextension/src/cta/payback.tsx b/packages/taler-wallet-webextension/src/cta/payback.tsx
deleted file mode 100644
index 1e27fd912..000000000
--- a/packages/taler-wallet-webextension/src/cta/payback.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- This file is part of TALER
- (C) 2017 Inria
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { JSX } from "preact/jsx-runtime";
-import { h } from 'preact';
-
-/**
- * View and edit auditors.
- *
- * @author Florian Dold
- */
-
-/**
- * Imports.
- */
-
-export function makePaybackPage(): JSX.Element {
- return <div>not implemented</div>;
-}
diff --git a/packages/taler-wallet-webextension/src/cta/reset-required.tsx b/packages/taler-wallet-webextension/src/cta/reset-required.tsx
deleted file mode 100644
index e66c0db57..000000000
--- a/packages/taler-wallet-webextension/src/cta/reset-required.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- This file is part of TALER
- (C) 2017 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Page to inform the user when a database reset is required.
- *
- * @author Florian Dold
- */
-
-import { Component, JSX, h } from "preact";
-import * as wxApi from "../wxApi";
-
-interface State {
- /**
- * Did the user check the confirmation check box?
- */
- checked: boolean;
-
- /**
- * Do we actually need to reset the db?
- */
- resetRequired: boolean;
-}
-
-class ResetNotification extends Component<any, State> {
- constructor(props: any) {
- super(props);
- this.state = { checked: false, resetRequired: true };
- setInterval(() => this.update(), 500);
- }
- async update(): Promise<void> {
- const res = await wxApi.checkUpgrade();
- this.setState({ resetRequired: res.dbResetRequired });
- }
- render(): JSX.Element {
- if (this.state.resetRequired) {
- return (
- <div>
- <h1>Manual Reset Required</h1>
- <p>
- The wallet&apos;s database in your browser is incompatible with the{" "}
- currently installed wallet. Please reset manually.
- </p>
- <p>
- Once the database format has stabilized, we will provide automatic
- upgrades.
- </p>
- <input
- id="check"
- type="checkbox"
- checked={this.state.checked}
- onChange={() => {
- this.setState(prev => ({ checked: prev.checked }))
- }}
- />{" "}
- <label htmlFor="check">
- I understand that I will lose all my data
- </label>
- <br />
- <button
- class="pure-button"
- disabled={!this.state.checked}
- onClick={() => wxApi.resetDb()}
- >
- Reset
- </button>
- </div>
- );
- }
- return (
- <div>
- <h1>Everything is fine!</h1>A reset is not required anymore, you can
- close this page.
- </div>
- );
- }
-}
-
-/**
- * @deprecated to be removed
- */
-export function createResetRequiredPage(): JSX.Element {
- return <ResetNotification />;
-}
diff --git a/packages/taler-wallet-webextension/src/cta/return-coins.tsx b/packages/taler-wallet-webextension/src/cta/return-coins.tsx
deleted file mode 100644
index 43d73b5fe..000000000
--- a/packages/taler-wallet-webextension/src/cta/return-coins.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- This file is part of TALER
- (C) 2017 Inria
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { JSX } from "preact/jsx-runtime";
-import { h } from 'preact';
-/**
- * Return coins to own bank account.
- *
- * @author Florian Dold
- */
-
-/**
- * Imports.
- */
-export function createReturnCoinsPage(): JSX.Element {
- return <span>Not implemented yet.</span>;
-}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx b/packages/taler-wallet-webextension/src/cta/termsExample.ts
index 5e29a3e39..ba0bee89e 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/termsExample.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -13,29 +13,14 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+/* eslint-disable no-useless-escape */
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { amountFractionalBase, Amounts } from '@gnu-taler/taler-util';
-import { ExchangeRecord } from '@gnu-taler/taler-wallet-core';
-import { ExchangeWithdrawDetails } from '@gnu-taler/taler-wallet-core/src/operations/withdraw';
-import { getMaxListeners } from 'process';
-import { createExample } from '../test-utils';
-import { View as TestedComponent } from './Withdraw';
-
-
-export default {
- title: 'cta/withdraw',
- component: TestedComponent,
- argTypes: {
- onSwitchExchange: { action: 'onRetry' },
- },
-};
-
-const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Terms Of Service &#8212; Taler Terms of Service</title>
@@ -48,14 +33,14 @@ const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
</div>
</body>
</html>
-`
-const termsPlain = `
+`;
+export const termsPlain = `
Terms Of Service
****************
Last Updated: 12.4.2019
-Welcome! Taler Systems SA (“we,” “our,” or “us”) provides a payment
+Welcome! Taler Systems S.A. (“we,” “our,” or “us”) provides a payment
service through our Internet presence (collectively the “Services”).
Before using our Services, please read the Terms of Service (the
“Terms” or the “Agreement”) carefully.
@@ -206,7 +191,7 @@ strong copyleft license, which means that any derivative works must be
distributed under the same license terms as the original software. If
you have any questions, you should review the GNU GPL’s full terms and
conditions at https://www.gnu.org/licenses/gpl-3.0.en.html. “Taler”
-itself is a trademark of Taler Systems SA. You are welcome to use the
+itself is a trademark of Taler Systems S.A.. You are welcome to use the
name in relation to processing payments using the Taler protocol,
assuming your use is compatible with an official release from the GNU
Project that is not older than two years.
@@ -432,16 +417,16 @@ Questions or comments
We welcome comments, questions, concerns, or suggestions. Please send
us a message on our contact page at legal@taler-systems.com.
-`
+`;
-const termsXml = `<?xml version="1.0" encoding="utf-8"?>
+export const termsXml = `<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE document PUBLIC "+//IDN docutils.sourceforge.net//DTD Docutils Generic//EN//XML" "http://docutils.sourceforge.net/docs/ref/docutils.dtd">
<!-- Generated by Docutils 0.14 -->
<document source="/home/grothoff/research/taler/exchange/contrib/tos/tos.rst">
<section ids="terms-of-service" names="terms\ of\ service">
<title>Terms Of Service</title>
<paragraph>Last Updated: 12.4.2019</paragraph>
- <paragraph>Welcome! Taler Systems SA (“we,” “our,” or “us”) provides a payment service
+ <paragraph>Welcome! Taler Systems S.A. (“we,” “our,” or “us”) provides a payment service
through our Internet presence (collectively the “Services”). Before using our
Services, please read the Terms of Service (the “Terms” or the “Agreement”)
carefully.</paragraph>
@@ -574,7 +559,7 @@ const termsXml = `<?xml version="1.0" encoding="utf-8"?>
license terms as the original software. If you have any questions, you should
review the GNU GPL’s full terms and conditions at
<reference refuri="https://www.gnu.org/licenses/gpl-3.0.en.html">https://www.gnu.org/licenses/gpl-3.0.en.html</reference>. “Taler” itself is a trademark
- of Taler Systems SA. You are welcome to use the name in relation to processing
+ of Taler Systems S.A.. You are welcome to use the name in relation to processing
payments using the Taler protocol, assuming your use is compatible with an
official release from the GNU Project that is not older than two years.</paragraph>
</section>
@@ -780,123 +765,7 @@ const termsXml = `<?xml version="1.0" encoding="utf-8"?>
</document>
`;
-export const NewTerms = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: 0,
- value: 0
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
-
- onSwitchExchange: async () => { },
- terms: {
- value: {
- type: 'xml',
- document: new DOMParser().parseFromString(termsXml, "text/xml"),
- },
- status: 'new'
- },
-})
-
-export const TermsReviewingPLAIN = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: 0,
- value: 0
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
-
- onSwitchExchange: async () => { },
- terms: {
- value: {
- type: 'plain',
- content: termsPlain
- },
- status: 'new'
- },
- reviewing: true
-})
-
-export const TermsReviewingHTML = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: 0,
- value: 0
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
-
- onSwitchExchange: async () => { },
- terms: {
- value: {
- type: 'html',
- href: new URL(`data:text/html;base64,${Buffer.from(termsHtml).toString('base64')}`),
- },
- status: 'new'
- },
- reviewing: true
-})
-
-const termsPdf = `
+export const termsPdf = `
%PDF-1.2
9 0 obj << >>
stream
@@ -909,306 +778,4 @@ endobj
trailer
<< /Root 3 0 R >>
%%EOF
-`
-
-export const TermsReviewingPDF = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: 0,
- value: 0
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
-
- onSwitchExchange: async () => { },
- terms: {
- value: {
- type: 'pdf',
- location: new URL(`data:text/html;base64,${Buffer.from(termsPdf).toString('base64')}`),
- },
- status: 'new'
- },
- reviewing: true
-})
-
-
-export const TermsReviewingXML = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: 0,
- value: 0
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
-
- onSwitchExchange: async () => { },
- terms: {
- value: {
- type: 'xml',
- document: new DOMParser().parseFromString(termsXml, "text/xml"),
- },
- status: 'new'
- },
- reviewing: true
-})
-
-export const NewTermsAccepted = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: 0,
- value: 0
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
- onSwitchExchange: async () => { },
- terms: {
- value: {
- type: 'xml',
- document: new DOMParser().parseFromString(termsXml, "text/xml"),
- },
- status: 'new'
- },
- reviewed: true
-})
-
-export const TermsShowAgainXML = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: 0,
- value: 0
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
-
- onSwitchExchange: async () => { },
- terms: {
- value: {
- type: 'xml',
- document: new DOMParser().parseFromString(termsXml, "text/xml"),
- },
- status: 'new'
- },
- reviewed: true,
- reviewing: true,
-})
-
-export const TermsChanged = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: 0,
- value: 0
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
-
- onSwitchExchange: async () => { },
- terms: {
- value: {
- type: 'xml',
- document: new DOMParser().parseFromString(termsXml, "text/xml"),
- },
- status: 'changed'
- },
-})
-
-export const TermsNotFound = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: 0,
- value: 0
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
-
- onSwitchExchange: async () => { },
- terms: {
- status: 'notfound'
- },
-})
-
-export const TermsAlreadyAccepted = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: amountFractionalBase * 0.5,
- value: 0
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
-
- onSwitchExchange: async () => { },
- terms: {
- status: 'accepted'
- },
-})
-
-
-export const WithoutFee = createExample(TestedComponent, {
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'exchange.demo.taler.net',
- paytoUris: ['asd'],
- }, {
- currency: 'USD',
- exchangeBaseUrl: 'exchange.test.taler.net',
- paytoUris: ['asd'],
- }],
- exchangeBaseUrl: 'exchange.demo.taler.net',
- details: {
- content: '',
- contentType: '',
- currentEtag: '',
- acceptedEtag: undefined,
- },
- withdrawalFee: {
- currency: 'USD',
- fraction: 0,
- value: 0,
- },
- amount: {
- currency: 'USD',
- value: 2,
- fraction: 10000000
- },
-
- onSwitchExchange: async () => { },
- terms: {
- value: {
- type: 'xml',
- document: new DOMParser().parseFromString(termsXml, "text/xml"),
- },
- status: 'accepted',
- }
-}) \ No newline at end of file
+`;
diff --git a/packages/taler-wallet-webextension/src/custom.d.ts b/packages/taler-wallet-webextension/src/custom.d.ts
index 1981067d4..1bcd2a8d0 100644
--- a/packages/taler-wallet-webextension/src/custom.d.ts
+++ b/packages/taler-wallet-webextension/src/custom.d.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -17,11 +17,17 @@ declare module "*.jpeg" {
const content: any;
export default content;
}
+declare module "*.jpg" {
+ const content: any;
+ export default content;
+}
declare module "*.png" {
const content: any;
export default content;
}
-declare module '*.svg' {
+declare module "*.svg" {
const content: any;
export default content;
}
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
index 2131d45cb..bd430f2ef 100644
--- a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
@@ -1,48 +1,98 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ExchangesListRespose } from "@gnu-taler/taler-util";
-import { useEffect, useState } from "preact/hooks";
-import * as wxApi from "../wxApi";
+import { TalerErrorDetail, TalerError } from "@gnu-taler/taler-util";
+import { useEffect, useMemo, useState } from "preact/hooks";
+import { BackgroundError } from "../wxApi.js";
-interface HookOk<T> {
+export interface HookOk<T> {
hasError: false;
response: T;
}
-interface HookError {
+export type HookError = HookGenericError | HookOperationalError;
+
+export interface HookGenericError {
hasError: true;
+ type: "error";
message: string;
}
+export interface HookOperationalError {
+ hasError: true;
+ type: "taler";
+ message: string;
+ details: TalerErrorDetail;
+}
+
+interface WithRetry {
+ retry: () => void;
+}
+
export type HookResponse<T> = HookOk<T> | HookError | undefined;
+export type HookResponseWithRetry<T> =
+ | ((HookOk<T> | HookError) & WithRetry)
+ | undefined;
-export function useAsyncAsHook<T> (fn: (() => Promise<T>)): HookResponse<T> {
+export function useAsyncAsHook<T>(
+ fn: () => Promise<T | false>,
+ deps?: unknown[],
+): HookResponseWithRetry<T> {
const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
- useEffect(() => {
- async function doAsync() {
- try {
- const response = await fn();
- setHookResponse({ hasError: false, response });
- } catch (e) {
- if (e instanceof Error) {
- setHookResponse({ hasError: true, message: e.message });
- }
+
+ const args = useMemo(
+ () => ({
+ fn,
+ }),
+ deps || [],
+ );
+
+ async function doAsync(): Promise<void> {
+ try {
+ const response = await args.fn();
+ if (response === false) return;
+ setHookResponse({ hasError: false, response });
+ } catch (e) {
+ if (e instanceof TalerError) {
+ setHookResponse({
+ hasError: true,
+ type: "taler",
+ message: e.message,
+ details: e.errorDetail,
+ });
+ } else if (e instanceof BackgroundError) {
+ setHookResponse({
+ hasError: true,
+ type: "taler",
+ message: e.message,
+ details: e.errorDetail,
+ });
+ } else if (e instanceof Error) {
+ setHookResponse({
+ hasError: true,
+ type: "error",
+ message: e.message,
+ });
}
}
- doAsync()
- }, []);
- return result;
+ }
+
+ useEffect(() => {
+ doAsync();
+ }, [args]);
+
+ if (!result) return undefined;
+ return { ...result, retry: doAsync };
}
diff --git a/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts b/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts
index f3b1b3b5f..6288b6986 100644
--- a/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,37 +14,41 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
-import * as wxApi from "../wxApi";
-
+import { useBackendContext } from "../context/backend.js";
export interface BackupDeviceName {
name: string;
- update: (s:string) => Promise<void>
+ update: (s: string) => Promise<void>;
}
-
export function useBackupDeviceName(): BackupDeviceName {
const [status, setStatus] = useState<BackupDeviceName>({
- name: '',
- update: () => Promise.resolve()
- })
+ name: "",
+ update: () => Promise.resolve(),
+ });
+ const api = useBackendContext();
useEffect(() => {
- async function run() {
+ async function run(): Promise<void> {
//create a first list of backup info by currency
- const status = await wxApi.getBackupInfo()
-
- async function update(newName: string) {
- await wxApi.setWalletDeviceId(newName)
- setStatus(old => ({ ...old, name: newName }))
+ const status = await api.wallet.call(
+ WalletApiOperation.GetBackupInfo,
+ {},
+ );
+
+ async function update(newName: string): Promise<void> {
+ await api.wallet.call(WalletApiOperation.SetWalletDeviceId, {
+ walletDeviceId: newName,
+ });
+ setStatus((old) => ({ ...old, name: newName }));
}
- setStatus({ name: status.deviceId, update })
+ setStatus({ name: status.deviceId, update });
}
- run()
- }, [])
+ run();
+ }, []);
- return status
+ return status;
}
-
diff --git a/packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts b/packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts
deleted file mode 100644
index c46ab6a5f..000000000
--- a/packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { ProviderInfo, ProviderPaymentPaid, ProviderPaymentStatus, ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
-import { useEffect, useState } from "preact/hooks";
-import * as wxApi from "../wxApi";
-
-
-export interface BackupStatus {
- deviceName: string;
- providers: ProviderInfo[];
- sync: () => Promise<void>;
-}
-
-function getStatusTypeOrder(t: ProviderPaymentStatus) {
- return [
- ProviderPaymentType.InsufficientBalance,
- ProviderPaymentType.TermsChanged,
- ProviderPaymentType.Unpaid,
- ProviderPaymentType.Paid,
- ProviderPaymentType.Pending,
- ].indexOf(t.type)
-}
-
-function getStatusPaidOrder(a: ProviderPaymentPaid, b: ProviderPaymentPaid) {
- return a.paidUntil.t_ms === 'never' ? -1 :
- b.paidUntil.t_ms === 'never' ? 1 :
- a.paidUntil.t_ms - b.paidUntil.t_ms
-}
-
-export function useBackupStatus(): BackupStatus | undefined {
- const [status, setStatus] = useState<BackupStatus | undefined>(undefined)
-
- useEffect(() => {
- async function run() {
- //create a first list of backup info by currency
- const status = await wxApi.getBackupInfo()
-
- const providers = status.providers.sort((a, b) => {
- if (a.paymentStatus.type === ProviderPaymentType.Paid && b.paymentStatus.type === ProviderPaymentType.Paid) {
- return getStatusPaidOrder(a.paymentStatus, b.paymentStatus)
- }
- return getStatusTypeOrder(a.paymentStatus) - getStatusTypeOrder(b.paymentStatus)
- })
-
- async function sync() {
- await wxApi.syncAllProviders()
- }
-
- setStatus({ deviceName: status.deviceId, providers, sync })
- }
- run()
- }, [])
-
- return status
-}
-
-
diff --git a/packages/taler-wallet-webextension/src/hooks/useBalances.ts b/packages/taler-wallet-webextension/src/hooks/useBalances.ts
deleted file mode 100644
index 37424fb05..000000000
--- a/packages/taler-wallet-webextension/src/hooks/useBalances.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { BalancesResponse } from "@gnu-taler/taler-util";
-import { useEffect, useState } from "preact/hooks";
-import * as wxApi from "../wxApi";
-
-
-interface BalancesHookOk {
- hasError: false;
- response: BalancesResponse;
-}
-
-interface BalancesHookError {
- hasError: true;
- message: string;
-}
-
-export type BalancesHook = BalancesHookOk | BalancesHookError | undefined;
-
-export function useBalances(): BalancesHook {
- const [balance, setBalance] = useState<BalancesHook>(undefined);
- useEffect(() => {
- async function checkBalance() {
- try {
- const response = await wxApi.getBalance();
- console.log("got balance", balance);
- setBalance({ hasError: false, response });
- } catch (e) {
- console.error("could not retrieve balances", e);
- if (e instanceof Error) {
- setBalance({ hasError: true, message: e.message });
- }
- }
- }
- checkBalance()
- return wxApi.onUpdateNotification(checkBalance);
- }, []);
-
- return balance;
-}
diff --git a/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts
new file mode 100644
index 000000000..35b7148cc
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/hooks/useClipboardPermissions.ts
@@ -0,0 +1,76 @@
+/*
+ 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 { useEffect, useState } from "preact/hooks";
+import { useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { ToggleHandler } from "../mui/handlers.js";
+import { platform } from "../platform/foreground.js";
+
+/**
+ * This is not implemented.
+ * Clipboard permission need to get ask the permission to the user
+ * based on user-intention
+ * @returns
+ */
+export function useClipboardPermissions(): ToggleHandler {
+ const [enabled, setEnabled] = useState(false);
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+
+ async function handleClipboardPerm(): Promise<void> {
+ if (!enabled) {
+ // We set permissions here, since apparently FF wants this to be done
+ // as the result of an input event ...
+ let granted: boolean;
+ try {
+ granted = await platform
+ .getPermissionsApi()
+ .requestClipboardPermissions();
+ } catch (lastError) {
+ setEnabled(false);
+ throw lastError;
+ }
+ setEnabled(granted);
+ } else {
+ // try {
+ // await api.background
+ // .call("toggleHeaderListener", false)
+ // .then((r) => setEnabled(r.newValue));
+ // } catch (e) {
+ // }
+ }
+ return;
+ }
+
+ // useEffect(() => {
+ // async function getValue(): Promise<void> {
+ // const res = await api.background.call(
+ // "containsHeaderListener",
+ // undefined,
+ // );
+ // setEnabled(res.newValue);
+ // }
+ // getValue();
+ // }, []);
+
+ return {
+ value: enabled,
+ button: {
+ onClick: pushAlertOnError(handleClipboardPerm),
+ },
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts
deleted file mode 100644
index 888d4d5f1..000000000
--- a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { WalletDiagnostics } from "@gnu-taler/taler-util";
-import { useEffect, useState } from "preact/hooks";
-import * as wxApi from "../wxApi";
-
-export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {
- const [timedOut, setTimedOut] = useState(false);
- const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>(
- undefined
- );
-
- useEffect(() => {
- let gotDiagnostics = false;
- setTimeout(() => {
- if (!gotDiagnostics) {
- console.error("timed out");
- setTimedOut(true);
- }
- }, 1000);
- const doFetch = async (): Promise<void> => {
- const d = await wxApi.getDiagnostics();
- console.log("got diagnostics", d);
- gotDiagnostics = true;
- setDiagnostics(d);
- };
- console.log("fetching diagnostics");
- doFetch();
- }, []);
- return [diagnostics, timedOut]
-} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts
deleted file mode 100644
index a92425760..000000000
--- a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { useState, useEffect } from "preact/hooks";
-import * as wxApi from "../wxApi";
-import { getPermissionsApi } from "../compat";
-import { extendedPermissions } from "../permissions";
-
-
-export function useExtendedPermissions(): [boolean, () => void] {
- const [enabled, setEnabled] = useState(false);
-
- const toggle = () => {
- setEnabled(v => !v);
- handleExtendedPerm(enabled).then(result => {
- setEnabled(result);
- });
- };
-
- useEffect(() => {
- async function getExtendedPermValue(): Promise<void> {
- const res = await wxApi.getExtendedPermissions();
- setEnabled(res.newValue);
- }
- getExtendedPermValue();
- }, []);
- return [enabled, toggle];
-}
-
-async function handleExtendedPerm(isEnabled: boolean): Promise<boolean> {
- let nextVal: boolean | undefined;
-
- if (!isEnabled) {
- const granted = await new Promise<boolean>((resolve, reject) => {
- // We set permissions here, since apparently FF wants this to be done
- // as the result of an input event ...
- getPermissionsApi().request(extendedPermissions, (granted: boolean) => {
- if (chrome.runtime.lastError) {
- console.error("error requesting permissions");
- console.error(chrome.runtime.lastError);
- reject(chrome.runtime.lastError);
- return;
- }
- console.log("permissions granted:", granted);
- resolve(granted);
- });
- });
- const res = await wxApi.setExtendedPermissions(granted);
- nextVal = res.newValue;
- } else {
- const res = await wxApi.setExtendedPermissions(false);
- nextVal = res.newValue;
- }
- console.log("new permissions applied:", nextVal ?? false);
- return nextVal ?? false
-} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts b/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts
new file mode 100644
index 000000000..8d26bf3b6
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/hooks/useIsOnline.ts
@@ -0,0 +1,14 @@
+import { codecForBoolean } from "@gnu-taler/taler-util";
+import { buildStorageKey, useMemoryStorage } from "@gnu-taler/web-util/browser";
+import { platform } from "../platform/foreground.js";
+import { useEffect } from "preact/hooks";
+
+export function useIsOnline(): boolean {
+ const { value, update } = useMemoryStorage("online", true);
+ useEffect(() => {
+ return platform.listenNetworkConnectionState((state) => {
+ update(state === "on");
+ });
+ });
+ return value;
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts b/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
deleted file mode 100644
index 78a8b65d5..000000000
--- a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { StateUpdater, useState } from "preact/hooks";
-
-export function useLocalStorage(key: string, initialValue?: string): [string | undefined, StateUpdater<string | undefined>] {
- const [storedValue, setStoredValue] = useState<string | undefined>((): string | undefined => {
- return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
- });
-
- const setValue = (value?: string | ((val?: string) => string | undefined)) => {
- setStoredValue(p => {
- const toStore = value instanceof Function ? value(p) : value
- if (typeof window !== "undefined") {
- if (!toStore) {
- window.localStorage.removeItem(key)
- } else {
- window.localStorage.setItem(key, toStore);
- }
- }
- return toStore
- })
- };
-
- return [storedValue, setValue];
-}
-
-//TODO: merge with the above function
-export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
- const [storedValue, setStoredValue] = useState<string>((): string => {
- return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
- });
-
- const setValue = (value: string | ((val: string) => string)) => {
- const valueToStore = value instanceof Function ? value(storedValue) : value;
- setStoredValue(valueToStore);
- if (typeof window !== "undefined") {
- if (!valueToStore) {
- window.localStorage.removeItem(key)
- } else {
- window.localStorage.setItem(key, valueToStore);
- }
- }
- };
-
- return [storedValue, setValue];
-}
diff --git a/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts b/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
index 6520848a5..e2ba5b285 100644
--- a/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,9 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ProviderInfo } from "@gnu-taler/taler-wallet-core";
+import { ProviderInfo } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
-import * as wxApi from "../wxApi";
+import { useBackendContext } from "../context/backend.js";
export interface ProviderStatus {
info?: ProviderInfo;
@@ -26,31 +27,40 @@ export interface ProviderStatus {
export function useProviderStatus(url: string): ProviderStatus | undefined {
const [status, setStatus] = useState<ProviderStatus | undefined>(undefined);
-
+ const api = useBackendContext();
useEffect(() => {
- async function run() {
+ async function run(): Promise<void> {
//create a first list of backup info by currency
- const status = await wxApi.getBackupInfo();
+ const status = await api.wallet.call(
+ WalletApiOperation.GetBackupInfo,
+ {},
+ );
- const providers = status.providers.filter(p => p.syncProviderBaseUrl === url);
+ const providers = status.providers.filter(
+ (p) => p.syncProviderBaseUrl === url,
+ );
const info = providers.length ? providers[0] : undefined;
- async function sync() {
+ async function sync(): Promise<void> {
if (info) {
- await wxApi.syncOneProvider(info.syncProviderBaseUrl);
+ await api.wallet.call(WalletApiOperation.RunBackupCycle, {
+ providers: [info.syncProviderBaseUrl],
+ });
}
}
- async function remove() {
+ async function remove(): Promise<void> {
if (info) {
- await wxApi.removeProvider(info.syncProviderBaseUrl);
+ await api.wallet.call(WalletApiOperation.RemoveBackupProvider, {
+ provider: info.syncProviderBaseUrl,
+ });
}
}
setStatus({ info, sync, remove });
}
run();
- }, []);
+ });
return status;
}
diff --git a/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts b/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts
new file mode 100644
index 000000000..6907a247d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts
@@ -0,0 +1,141 @@
+/*
+ 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 { ExchangeListItem } from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
+import { useAlertContext } from "../context/alert.js";
+import { ButtonHandler } from "../mui/handlers.js";
+
+type State = State.Ready | State.NoExchangeFound | State.Selecting;
+
+export namespace State {
+ export interface NoExchangeFound {
+ status: "no-exchange-found";
+ error: undefined;
+ currency: string;
+ defaultExchange: string | undefined;
+ }
+ export interface Ready {
+ status: "ready";
+ doSelect: ButtonHandler;
+ selected: ExchangeListItem;
+ }
+ export interface Selecting {
+ status: "selecting-exchange";
+ error: undefined;
+ onSelection: (url: string) => Promise<void>;
+ onCancel: () => Promise<void>;
+ list: ExchangeListItem[];
+ currency: string;
+ initialValue: string;
+ }
+}
+
+interface Props {
+ currency: string;
+ //there is a preference for the default at the initial state
+ defaultExchange?: string;
+ //list of exchanges
+ list: ExchangeListItem[];
+}
+
+export function useSelectedExchange({
+ currency,
+ defaultExchange,
+ list,
+}: Props): State {
+ const [isSelecting, setIsSelecting] = useState(false);
+ const [selectedExchange, setSelectedExchange] = useState<string | undefined>(
+ undefined,
+ );
+ const { pushAlertOnError } = useAlertContext();
+
+ if (!list.length) {
+ return {
+ status: "no-exchange-found",
+ error: undefined,
+ currency,
+ defaultExchange,
+ };
+ }
+
+ const exchangesWithThisCurrency = list.filter((e) => e.currency === currency);
+ if (!exchangesWithThisCurrency.length) {
+ // there should be at least one exchange for this currency
+ return {
+ status: "no-exchange-found",
+ error: undefined,
+ currency,
+ defaultExchange,
+ };
+ }
+
+ if (isSelecting) {
+ const currentExchange =
+ selectedExchange ??
+ defaultExchange ??
+ exchangesWithThisCurrency[0].exchangeBaseUrl;
+ return {
+ status: "selecting-exchange",
+ error: undefined,
+ list: exchangesWithThisCurrency,
+ currency,
+ initialValue: currentExchange,
+ onSelection: async (exchangeBaseUrl: string) => {
+ setIsSelecting(false);
+ setSelectedExchange(exchangeBaseUrl);
+ },
+ onCancel: async () => {
+ setIsSelecting(false);
+ },
+ };
+ }
+
+ {
+ const found = !selectedExchange
+ ? undefined
+ : list.find((e) => e.exchangeBaseUrl === selectedExchange);
+ if (found)
+ return {
+ status: "ready",
+ doSelect: {
+ onClick: pushAlertOnError(async () => setIsSelecting(true)),
+ },
+ selected: found,
+ };
+ }
+ {
+ const found = !defaultExchange
+ ? undefined
+ : list.find((e) => e.exchangeBaseUrl === defaultExchange);
+ if (found)
+ return {
+ status: "ready",
+ doSelect: {
+ onClick: pushAlertOnError(async () => setIsSelecting(true)),
+ },
+ selected: found,
+ };
+ }
+
+ return {
+ status: "ready",
+ doSelect: {
+ onClick: pushAlertOnError(async () => setIsSelecting(true)),
+ },
+ selected: exchangesWithThisCurrency[0],
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useSettings.ts b/packages/taler-wallet-webextension/src/hooks/useSettings.ts
new file mode 100644
index 000000000..a79a71087
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/hooks/useSettings.ts
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { Settings, defaultSettings } from "../platform/api.js";
+import {
+ Codec,
+ buildCodecForObject,
+ codecForBoolean,
+} from "@gnu-taler/taler-util";
+
+function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
+ if (str === undefined) return undefined;
+ try {
+ return JSON.parse(str);
+ } catch {
+ return undefined;
+ }
+}
+
+export const codecForSettings = (): Codec<Settings> =>
+ buildCodecForObject<Settings>()
+ .property("walletAllowHttp", codecForBoolean())
+ .property("injectTalerSupport", codecForBoolean())
+ .property("autoOpen", codecForBoolean())
+ .property("advancedMode", codecForBoolean())
+ .property("backup", codecForBoolean())
+ .property("langSelector", codecForBoolean())
+ .property("showJsonOnError", codecForBoolean())
+ .property("extendedAccountTypes", codecForBoolean())
+ .property("suspendIndividualTransaction", codecForBoolean())
+ .property("showRefeshTransactions", codecForBoolean())
+ .property("showExchangeManagement", codecForBoolean())
+ .property("selectTosFormat", codecForBoolean())
+ .property("showWalletActivity", codecForBoolean())
+ .build("Settings");
+
+const SETTINGS_KEY = buildStorageKey("wallet-settings", codecForSettings());
+
+export function useSettings(): [
+ Readonly<Settings>,
+ <T extends keyof Settings>(key: T, value: Settings[T]) => void,
+] {
+ const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings);
+
+ function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
+ update({ ...value, [k]: v });
+ }
+
+ return [value, updateField];
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts
new file mode 100644
index 000000000..0bb47530c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts
@@ -0,0 +1,58 @@
+/*
+ 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 { expect } from "chai";
+import { h, VNode } from "preact";
+import { IoCProviderForTesting } from "../context/iocContext.js";
+import { useTalerActionURL } from "./useTalerActionURL.js";
+import * as tests from "@gnu-taler/web-util/testing";
+
+describe("useTalerActionURL hook", () => {
+ it("should be set url to undefined when dismiss", async () => {
+ const ctx = ({ children }: { children: any }): VNode => {
+ return h(IoCProviderForTesting, {
+ value: {
+ findTalerUriInActiveTab: async () => "asd",
+ findTalerUriInClipboard: async () => "qwe",
+ },
+ children,
+ });
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useTalerActionURL,
+ {},
+ [
+ ([url]) => {
+ expect(url).undefined;
+ },
+ ([url, setDismissed]) => {
+ expect(url).deep.equals({
+ location: "clipboard",
+ uri: "qwe",
+ });
+ setDismissed(true);
+ },
+ ([url]) => {
+ if (url !== undefined) throw Error("invalid");
+ expect(url).undefined;
+ },
+ ],
+ ctx,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts
index ff9cc029a..39b76c341 100644
--- a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,46 +14,45 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
+import { useIocContext } from "../context/iocContext.js";
-export function useTalerActionURL(): [string | undefined, (s: boolean) => void] {
- const [talerActionUrl, setTalerActionUrl] = useState<string | undefined>(
- undefined
+export interface UriLocation {
+ uri: string;
+ location: "clipboard" | "activeTab";
+}
+
+export function useTalerActionURL(): [
+ UriLocation | undefined,
+ (s: boolean) => void,
+] {
+ const [talerActionUrl, setTalerActionUrl] = useState<UriLocation | undefined>(
+ undefined,
);
const [dismissed, setDismissed] = useState(false);
+ const { findTalerUriInActiveTab, findTalerUriInClipboard } = useIocContext();
useEffect(() => {
async function check(): Promise<void> {
- const talerUri = await findTalerUriInActiveTab();
- setTalerActionUrl(talerUri)
+ const clipUri = await findTalerUriInClipboard();
+ if (clipUri) {
+ setTalerActionUrl({
+ location: "clipboard",
+ uri: clipUri,
+ });
+ return;
+ }
+ const tabUri = await findTalerUriInActiveTab();
+ if (tabUri) {
+ setTalerActionUrl({
+ location: "activeTab",
+ uri: tabUri,
+ });
+ return;
+ }
}
check();
}, []);
+
const url = dismissed ? undefined : talerActionUrl;
return [url, setDismissed];
}
-
-async function findTalerUriInActiveTab(): Promise<string | undefined> {
- return new Promise((resolve, reject) => {
- chrome.tabs.executeScript(
- {
- code: `
- (() => {
- let x = document.querySelector("a[href^='taler://'") || document.querySelector("a[href^='taler+http://'");
- return x ? x.href.toString() : null;
- })();
- `,
- allFrames: false,
- },
- (result) => {
- if (chrome.runtime.lastError) {
- console.error(chrome.runtime.lastError);
- resolve(undefined);
- return;
- }
- console.log("got result", result);
- resolve(result[0]);
- },
- );
- });
-}
diff --git a/packages/taler-wallet-webextension/src/i18n/de.po b/packages/taler-wallet-webextension/src/i18n/de.po
index bb355403d..bc66f2136 100644
--- a/packages/taler-wallet-webextension/src/i18n/de.po
+++ b/packages/taler-wallet-webextension/src/i18n/de.po
@@ -1,344 +1,2078 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
#
-# TALER is free software; you can redistribute it and/or modify it under the
+# GNU Taler is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3, or (at your option) any later version.
#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"PO-Revision-Date: 2024-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"
+"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.4.3\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Guthaben"
-#: src/util/wire.ts:37
+#: src/NavigationBar.tsx:142
#, c-format
-msgid "Invalid Wire"
+msgid "Backup"
+msgstr "Backup"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
msgstr ""
-#: src/util/wire.ts:42 src/util/wire.ts:45
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Einstellungen"
+
+#: src/NavigationBar.tsx:184
#, c-format
-msgid "Invalid Test Wire Detail"
+msgid "Dev"
+msgstr "Dev"
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
msgstr ""
-#: src/util/wire.ts:47
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Lädt Daten"
+
+#: src/wallet/BackupPage.tsx:123
#, c-format
-msgid "Test Wire Acct #%1$s on %2$s"
+msgid "Could not load backup providers"
msgstr ""
-#: src/util/wire.ts:49
+#: src/wallet/BackupPage.tsx:202
#, c-format
-msgid "Unknown Wire Detail"
+msgid "No backup providers configured"
msgstr ""
-#: src/webex/pages/benchmark.tsx:52
+#: src/wallet/BackupPage.tsx:205
#, c-format
-msgid "Operation"
+msgid "Add provider"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms "
+"of service"
+msgstr ""
+"Die AGB sind geändert worden, eine Weiternutzung des Diensts erfordert die "
+"Einwilligung in die neuen Allgemeinen Geschäftsbedingungen (AGB)"
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
msgstr ""
-#: src/webex/pages/benchmark.tsx:53
+#: src/wallet/ProviderDetailPage.tsx:290
#, c-format
-msgid "time (ms/op)"
+msgid "Backup valid until"
msgstr ""
-#: src/webex/pages/pay.tsx:130
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Zurück"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Seite der Reserve aufrufen"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Seite für Zahlungen aufrufen"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Seite für Rückerstattungen aufrufen"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "Seite der Aufwandsentschädigungen aufrufen"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Abhebeseite öffnen"
+
+#: src/popup/NoBalanceHelp.tsx:43
#, fuzzy, c-format
-msgid "The merchant %1$s offers you to purchase:"
-msgstr "Der Händler %1$s möchte einen Vertrag über %2$s mit Ihnen abschließen."
+msgid "Get digital cash"
+msgstr "Digitales Bargeld abheben"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "Konnte die Umsatzanzeige nicht laden"
-#: src/webex/pages/pay.tsx:136
+#: src/popup/BalancePage.tsx:175
#, c-format
-msgid "The total price is %1$s (plus %2$s fees)."
+msgid "Add"
msgstr ""
-#: src/webex/pages/pay.tsx:141
+#: src/popup/BalancePage.tsx:179
#, c-format
-msgid "The total price is %1$s."
+msgid "Send %1$s"
msgstr ""
-#: src/webex/pages/pay.tsx:163
+#: src/popup/TalerActionFound.tsx:44
#, c-format
-msgid "Retry"
+msgid "Taler Action"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
msgstr ""
-#: src/webex/pages/pay.tsx:173
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
#, fuzzy, c-format
-msgid "Confirm payment"
-msgstr "Bezahlung bestätigen"
+msgid "Could not load purchase proposal details"
+msgstr "Konnte die Umsatzanzeige nicht laden"
-#: src/webex/pages/popup.tsx:153
+#: src/components/ShowFullContractTermPopup.tsx:183
#, c-format
-msgid "Balance"
-msgstr "Saldo"
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr "Zusammenfassung"
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr "Betrag"
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr "Lieferdatum"
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr "Adresse digitaler Dienstleistung (Fulfillment-URL)"
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, fuzzy, c-format
+msgid "Exchanges"
+msgstr "Exchange"
-#: src/webex/pages/popup.tsx:154
+#: src/components/Part.tsx:148
#, c-format
-msgid "History"
-msgstr "Verlauf"
+msgid "Bank account"
+msgstr ""
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
-#: src/webex/pages/popup.tsx:155
+#: src/components/Part.tsx:163
#, c-format
-msgid "Debug"
-msgstr "Debug"
+msgid "IBAN"
+msgstr ""
-#: src/webex/pages/popup.tsx:175
+#: src/cta/Deposit/views.tsx:38
#, fuzzy, c-format
-msgid "You have no balance to show. Need some %1$s getting started?"
-msgstr "Sie haben kein Digitalgeld. Wollen Sie %1$s? abheben?"
+msgid "Could not load deposit status"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
-#: src/webex/pages/popup.tsx:238
+#: src/cta/Deposit/views.tsx:73
#, c-format
-msgid "%1$s incoming"
+msgid "To be received"
msgstr ""
-#: src/webex/pages/popup.tsx:250
+#: src/cta/Deposit/views.tsx:84
#, c-format
-msgid "%1$s being spent"
+msgid "Send &nbsp; %1$s"
msgstr ""
-#: src/webex/pages/popup.tsx:281
+#: src/components/BankDetailsByPaytoType.tsx:63
#, c-format
-msgid "Error: could not retrieve balance information."
+msgid "Bitcoin transfer details"
msgstr ""
-#: src/webex/pages/popup.tsx:390
+#: src/components/BankDetailsByPaytoType.tsx:66
#, c-format
-msgid "Invalid "
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an "
+"minimum amount."
msgstr ""
-#: src/webex/pages/popup.tsx:396
+#: src/components/BankDetailsByPaytoType.tsx:74
#, c-format
-msgid "Fees "
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two "
+"additional recipient and copy addresses and amounts"
msgstr ""
-#: src/webex/pages/popup.tsx:434
+#: src/components/BankDetailsByPaytoType.tsx:98
#, c-format
-msgid "Refresh sessions has completed"
+msgid ""
+"Make sure the amount show %1$s BTC, else you have to change the base unit to "
+"BTC"
msgstr ""
-#: src/webex/pages/popup.tsx:451
+#: src/components/BankDetailsByPaytoType.tsx:110
#, c-format
-msgid "Order Refused"
+msgid "Account"
msgstr ""
-#: src/webex/pages/popup.tsx:465
+#: src/components/BankDetailsByPaytoType.tsx:116
#, c-format
-msgid "Order redirected"
+msgid "Bank host"
msgstr ""
-#: src/webex/pages/popup.tsx:482
+#: src/components/BankDetailsByPaytoType.tsx:139
#, c-format
-msgid "Payment aborted"
+msgid "Bank transfer details"
msgstr ""
-#: src/webex/pages/popup.tsx:512
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr "Verwendungszweck"
+
+#: src/components/BankDetailsByPaytoType.tsx:154
#, c-format
-msgid "Payment Sent"
+msgid "Receiver name"
msgstr ""
-#: src/webex/pages/popup.tsx:536
+#: src/wallet/Transaction.tsx:98
#, c-format
-msgid "Order accepted"
+msgid "Could not load the transaction information"
msgstr ""
-#: src/webex/pages/popup.tsx:547
+#: src/wallet/Transaction.tsx:191
#, c-format
-msgid "Reserve balance updated"
+msgid "There was an error trying to complete the transaction"
msgstr ""
-#: src/webex/pages/popup.tsx:559
+#: src/wallet/Transaction.tsx:200
#, c-format
-msgid "Payment refund"
+msgid "This transaction is not completed"
msgstr ""
-#: src/webex/pages/popup.tsx:584
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr "Erneut versuchen"
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to "
+"get the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr "Bestätigen"
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr "Abheben"
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and "
+"check there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid ""
+"Bank has confirmed the wire transfer. Waiting for the exchange to send the "
+"coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr "Zahlung"
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
#, fuzzy, c-format
-msgid "Withdrawn"
-msgstr "Abheben bei %1$s"
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+"%1$s\n"
+" möchte einen Vertrag über %2$s\n"
+" mit Ihnen abschließen."
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid ""
+"Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
-#: src/webex/pages/popup.tsx:596
+#: src/wallet/Transaction.tsx:420
#, c-format
-msgid "Tip Accepted"
+msgid "Offer"
msgstr ""
-#: src/webex/pages/popup.tsx:606
+#: src/wallet/Transaction.tsx:431
#, c-format
-msgid "Tip Declined"
+msgid "Accept"
msgstr ""
-#: src/webex/pages/popup.tsx:615
+#: src/wallet/Transaction.tsx:438
#, c-format
-msgid "%1$s"
+msgid "Merchant"
msgstr ""
-#: src/webex/pages/popup.tsx:707
+#: src/wallet/Transaction.tsx:443
#, c-format
-msgid "Your wallet has no events recorded."
-msgstr "Ihre Geldbörse verzeichnet keine Vorkommnisse."
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
-#: src/webex/pages/return-coins.tsx:124
+#: src/wallet/Transaction.tsx:568
#, c-format
-msgid "Wire to bank account"
+msgid "Purchase summary"
msgstr ""
-#: src/webex/pages/return-coins.tsx:206
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr "Exchange"
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
#, fuzzy, c-format
-msgid "Confirm"
-msgstr "Bezahlung bestätigen"
+msgid "Country"
+msgstr "Betrag"
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr "Datum"
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
-#: src/webex/pages/return-coins.tsx:209
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr "Abheben"
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr "Rückerstattet"
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
#, fuzzy, c-format
-msgid "Cancel"
-msgstr "Saldo"
+msgid "Total transfer"
+msgstr "Insgesamt abgehoben"
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
-#: src/webex/pages/withdraw.tsx:73
+#: src/cta/Payment/views.tsx:263
#, c-format
-msgid "Could not get details for withdraw operation:"
+msgid "Already paid, you are going to be redirected to %1$s"
msgstr ""
-#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#: src/cta/Payment/views.tsx:274
#, c-format
-msgid "Chose different exchange provider"
+msgid "Already paid"
msgstr ""
-#: src/webex/pages/withdraw.tsx:109
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
#, c-format
msgid ""
-"Please select an exchange. You can review the details before after your "
-"selection."
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, fuzzy, c-format
+msgid "Your current balance is not enough."
+msgstr "Es gibt kein Guthaben anzuzeigen."
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
msgstr ""
-#: src/webex/pages/withdraw.tsx:121
+#: src/cta/Refund/views.tsx:34
+#, fuzzy, c-format
+msgid "Could not load refund status"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/cta/Refund/views.tsx:48
#, c-format
-msgid "Select %1$s"
+msgid "Digital cash refund"
msgstr ""
-#: src/webex/pages/withdraw.tsx:143
+#: src/cta/Refund/views.tsx:52
#, c-format
-msgid "Select custom exchange"
+msgid "You&apos;ve ignored the tip."
msgstr ""
-#: src/webex/pages/withdraw.tsx:163
+#: src/cta/Refund/views.tsx:70
#, c-format
-msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, fuzzy, c-format
+msgid "Total to refund"
+msgstr "Insgesamt abgehoben"
+
+#: src/cta/Refund/views.tsx:106
+#, fuzzy, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
msgstr ""
+"Der Händler %1$s bietet Ihnen eine Aufwandsentschädigung von %2$s durch den "
+"Exchange %3$s"
-#: src/webex/pages/withdraw.tsx:174
+#: src/cta/Refund/views.tsx:115
#, c-format
-msgid "Accept fees and withdraw"
+msgid "Order amount"
msgstr ""
-#: src/webex/pages/withdraw.tsx:192
+#: src/cta/Refund/views.tsx:122
#, c-format
-msgid "Cancel withdraw operation"
+msgid "Already refunded"
msgstr ""
-#: src/webex/renderHtml.tsx:249
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, fuzzy, c-format
+msgid "Could not load tip status"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
#, fuzzy, c-format
-msgid "Withdrawal fees:"
-msgstr "Abheben bei"
+msgid "The merchant is offering you a tip"
+msgstr ""
+"Der Händler %1$s bietet Ihnen eine Aufwandsentschädigung von %2$s durch den "
+"Exchange %3$s"
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, fuzzy, c-format
+msgid "Could not load"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr "Allgemeine Geschäftsbedingungen (AGB) anzeigen"
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr "Ich akzeptiere die Allgemeinen Geschäftsbedingungen (AGB)"
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr "Dieser Exchange hat keine Allgemeine Geschäftsbedingungen (AGB)"
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr "Allgemeine Geschäftsbedingungen (AGB) ansehen"
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr "Neue Version der Allgemeinen Geschäftsbedingungen (AGB) ansehen"
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+"Der Exchange wird mit Allgemeinen Geschäftsbedingungen ohne Inhalt antworten"
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr "Allgemeine Geschäftsbedingungen (AGB) herunterladen"
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr "Allgemeine Geschäftsbedingungen (AGB) verstecken"
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, fuzzy, c-format
+msgid "Could not load exchange fees"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr "Schließen"
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, fuzzy, c-format
+msgid "could not find any exchange"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, fuzzy, c-format
+msgid "Deposits"
+msgstr "%1$s zahlen"
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, fuzzy, c-format
+msgid "Withdrawals"
+msgstr "Abheben"
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and "
+"is valid for a period of time. The exchange will charge the indicated amount "
+"every time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is "
+"valid for a period of time. The exchange will charge the indicated amount "
+"every time a transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
-#: src/webex/renderHtml.tsx:252
+#: src/cta/Withdraw/views.tsx:60
#, c-format
-msgid "Rounding loss:"
+msgid "Could not get info of withdrawal"
msgstr ""
-#: src/webex/renderHtml.tsx:254
+#: src/cta/Withdraw/views.tsx:74
#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
+msgid "Digital cash withdrawal"
msgstr ""
-#: src/webex/renderHtml.tsx:262
+#: src/cta/Withdraw/views.tsx:79
#, c-format
-msgid "# Coins"
+msgid "Could not finish the withdrawal operation"
msgstr ""
-#: src/webex/renderHtml.tsx:263
+#: src/cta/Withdraw/views.tsx:127
#, c-format
-msgid "Value"
+msgid "Age restriction"
msgstr ""
-#: src/webex/renderHtml.tsx:264
+#: src/cta/Withdraw/views.tsx:145
#, fuzzy, c-format
-msgid "Withdraw Fee"
+msgid "Withdraw &nbsp; %1$s"
msgstr "Abheben bei %1$s"
-#: src/webex/renderHtml.tsx:265
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, fuzzy, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr "Manuelles Abheben"
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will "
+"send the coins to this wallet after receiving a wire transfer with the "
+"correct subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr "Abhebung beginnen"
+
+#: src/wallet/DepositPage/views.tsx:38
+#, fuzzy, c-format
+msgid "Could not load deposit balance"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, fuzzy, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr "Einlösen %1$s %2$s"
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr "Allgemeine Geschäftsbedingungen (AGB) ansehen"
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
#, c-format
-msgid "Refresh Fee"
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid ""
+"To complete the process you need to wire%1$s %2$s to the exchange bank "
+"account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a "
+"banking app installed that supports RFC 8905"
msgstr ""
-#: src/webex/renderHtml.tsx:266
+#: src/wallet/ReserveCreated.tsx:98
#, c-format
-msgid "Deposit Fee"
+msgid "Cancel withdrawal"
msgstr ""
+#: src/wallet/Settings.tsx:115
#, fuzzy, c-format
+msgid "Could not toggle auto-open"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/wallet/Settings.tsx:121
+#, fuzzy, c-format
+msgid "Could not toggle clipboard"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+"Die Diagnostik ist abgeschlossen. Es war keine Kommunikation mit dem Wallet-"
+"Backend möglich."
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr "Ein Problem wurde festgestellt:"
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check "
+"the preference name %2$s)."
+msgstr ""
+"Bitte prüfen Sie ihre %1$s Einstellungen, für die Sie IndexedDB verwenden ("
+"preference name %2$s prüfen)."
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+"Die Datenbank des Wallets ist veraltet. Aktuell wird jedoch keine Migration "
+"auf eine neue Version unterstützt. Bitte wählen Sie %1$s zum Zurücksetzen "
+"der Wallet-Datenbank."
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr "Diagnostik wird durchgeführt"
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr "Debugging-Tools"
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE "
+"ALL YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr "zurücksetzen"
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Noch zu vergeben"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, fuzzy, c-format
+msgid "Could not load list of exchange"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, fuzzy, c-format
+msgid "Could not load backup recovery information"
+msgstr "Konnte die Umsatzanzeige nicht laden"
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
+
+#~ msgid "Back"
+#~ msgstr "Zurück"
+
+#~ msgid ""
+#~ "To withdraw money you can start from your bank site or click the "
+#~ "\"withdraw\" button to use a known exchange."
+#~ msgstr ""
+#~ "Zum Abheben von digitalem Geld bitte von der Bank-Seite aus starten oder "
+#~ "\"Abheben\" drücken, um einen schon bekannten Exchange zu verwenden."
+
+#~ msgid "Enter URI"
+#~ msgstr "URI eingeben"
+
+#~ msgid "no balance"
+#~ msgstr "Kein Guthaben"
+
+#~ msgid "Withdraw anyway"
+#~ msgstr "Trotzdem abheben"
+
+#~ msgid "Invalid Wire"
+#~ msgstr "Ungültige Überweisung"
+
+#~ msgid "Invalid Test Wire Detail"
+#~ msgstr "Ungültige Überweisungsdaten"
+
+#~ msgid "Unknown Wire Detail"
+#~ msgstr "Unbekannte Überweisungsdaten"
+
+#~ msgid "The total price is %1$s (plus %2$s fees)."
+#~ msgstr "Gesamtbetrag %1$s (zuzüglich %2$s Gebühren)."
+
+#~ msgid "The total price is %1$s."
+#~ msgstr "Gesamter Zahlbetrag: %1$s."
+
+#~ msgid "Confirm payment"
+#~ msgstr "Zahlung bestätigen"
+
+#~ msgid "History"
+#~ msgstr "Verlauf"
+
+#~ msgid "%1$s incoming"
+#~ msgstr "%1$s empfangen"
+
+#~ msgid "%1$s being spent"
+#~ msgstr "%1$s ausgezahlt"
+
+#~ msgid "Your wallet has no events recorded."
+#~ msgstr "Ihre Geldbörse verzeichnet keine Vorkommnisse."
+
+#, fuzzy
#~ msgid "Bank requested reserve (%1$s) for %2$s."
#~ msgstr "Bank bestätig anlegen der Reserve (%1$s) bei %2$s"
-#, fuzzy, c-format
+#, fuzzy
#~ msgid "Started to withdraw %1$s from %2$s (%3$s)."
#~ msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt"
-#, fuzzy, c-format
+#, fuzzy
#~ msgid "Merchant %1$s offered contract %2$s."
#~ msgstr ""
#~ "%1$s\n"
#~ " möchte einen Vertrag über %2$s\n"
#~ " mit Ihnen abschließen."
-#, fuzzy, c-format
+#, fuzzy
#~ msgid "Withdrew %1$s from %2$s ( %3$s)."
#~ msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt"
-#, fuzzy, c-format
+#, fuzzy
#~ msgid "Paid %1$s to merchant %2$s.%3$s( %4$s)"
#~ msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt"
-#, fuzzy, c-format
+#, fuzzy
#~ msgid "Merchant %1$s gave a refund over %2$s."
#~ msgstr ""
#~ "%1$s\n"
#~ " möchte einen Vertrag über %2$s\n"
#~ " mit Ihnen abschließen."
-#, fuzzy, c-format
-#~ msgid "Merchant %1$s gave a %2$s of %3$s."
-#~ msgstr ""
-#~ "%1$s\n"
-#~ " möchte einen Vertrag über %2$s\n"
-#~ " mit Ihnen abschließen."
-
-#, fuzzy, c-format
+#, fuzzy
#~ msgid "Submitting payment"
#~ msgstr "Bezahlung bestätigen"
-#, fuzzy, c-format
+#, fuzzy
#~ msgid "Aborting payment ..."
#~ msgstr "Bezahlung bestätigen"
-#, fuzzy, c-format
-#~ msgid "Retry Payment"
-#~ msgstr "Bezahlung bestätigen"
-
-#, fuzzy, c-format
+#, fuzzy
#~ msgid "Abort Payment"
#~ msgstr "Bezahlung bestätigen"
@@ -346,10 +2080,6 @@ msgstr ""
#~ msgid "You are about to purchase:"
#~ msgstr "Sie sind dabei, Folgendes zu kaufen:"
-#, fuzzy
-#~ msgid "Withdrawal fees: %1$s"
-#~ msgstr "Abheben bei %1$s"
-
#~ msgid "Wallet depleted reserve (%1$s) at %2$s"
#~ msgstr "Geldbörse hat die Reserve (%1$s) erschöpft"
diff --git a/packages/taler-wallet-webextension/src/i18n/en-US.po b/packages/taler-wallet-webextension/src/i18n/en-US.po
deleted file mode 100644
index 4fe38d5e9..000000000
--- a/packages/taler-wallet-webextension/src/i18n/en-US.po
+++ /dev/null
@@ -1,294 +0,0 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: src/util/wire.ts:37
-#, c-format
-msgid "Invalid Wire"
-msgstr ""
-
-#: src/util/wire.ts:42 src/util/wire.ts:45
-#, c-format
-msgid "Invalid Test Wire Detail"
-msgstr ""
-
-#: src/util/wire.ts:47
-#, c-format
-msgid "Test Wire Acct #%1$s on %2$s"
-msgstr ""
-
-#: src/util/wire.ts:49
-#, c-format
-msgid "Unknown Wire Detail"
-msgstr ""
-
-#: src/webex/pages/benchmark.tsx:52
-#, c-format
-msgid "Operation"
-msgstr ""
-
-#: src/webex/pages/benchmark.tsx:53
-#, c-format
-msgid "time (ms/op)"
-msgstr ""
-
-#: src/webex/pages/pay.tsx:130
-#, c-format
-msgid "The merchant %1$s offers you to purchase:"
-msgstr ""
-
-#: src/webex/pages/pay.tsx:136
-#, c-format
-msgid "The total price is %1$s (plus %2$s fees)."
-msgstr ""
-
-#: src/webex/pages/pay.tsx:141
-#, c-format
-msgid "The total price is %1$s."
-msgstr ""
-
-#: src/webex/pages/pay.tsx:163
-#, c-format
-msgid "Retry"
-msgstr ""
-
-#: src/webex/pages/pay.tsx:173
-#, c-format
-msgid "Confirm payment"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:153
-#, c-format
-msgid "Balance"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:154
-#, c-format
-msgid "History"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:155
-#, c-format
-msgid "Debug"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:175
-#, c-format
-msgid "You have no balance to show. Need some %1$s getting started?"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:238
-#, c-format
-msgid "%1$s incoming"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:250
-#, c-format
-msgid "%1$s being spent"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:281
-#, c-format
-msgid "Error: could not retrieve balance information."
-msgstr ""
-
-#: src/webex/pages/popup.tsx:390
-#, c-format
-msgid "Invalid "
-msgstr ""
-
-#: src/webex/pages/popup.tsx:396
-#, c-format
-msgid "Fees "
-msgstr ""
-
-#: src/webex/pages/popup.tsx:434
-#, c-format
-msgid "Refresh sessions has completed"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:451
-#, c-format
-msgid "Order Refused"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:465
-#, c-format
-msgid "Order redirected"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:482
-#, c-format
-msgid "Payment aborted"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:512
-#, c-format
-msgid "Payment Sent"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:536
-#, c-format
-msgid "Order accepted"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:547
-#, c-format
-msgid "Reserve balance updated"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:559
-#, c-format
-msgid "Payment refund"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:584
-#, c-format
-msgid "Withdrawn"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:596
-#, c-format
-msgid "Tip Accepted"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:606
-#, c-format
-msgid "Tip Declined"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:615
-#, c-format
-msgid "%1$s"
-msgstr ""
-
-#: src/webex/pages/popup.tsx:707
-#, c-format
-msgid "Your wallet has no events recorded."
-msgstr ""
-
-#: src/webex/pages/return-coins.tsx:124
-#, c-format
-msgid "Wire to bank account"
-msgstr ""
-
-#: src/webex/pages/return-coins.tsx:206
-#, c-format
-msgid "Confirm"
-msgstr ""
-
-#: src/webex/pages/return-coins.tsx:209
-#, c-format
-msgid "Cancel"
-msgstr ""
-
-#: src/webex/pages/withdraw.tsx:73
-#, c-format
-msgid "Could not get details for withdraw operation:"
-msgstr ""
-
-#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
-#, c-format
-msgid "Chose different exchange provider"
-msgstr ""
-
-#: src/webex/pages/withdraw.tsx:109
-#, c-format
-msgid ""
-"Please select an exchange. You can review the details before after your "
-"selection."
-msgstr ""
-
-#: src/webex/pages/withdraw.tsx:121
-#, c-format
-msgid "Select %1$s"
-msgstr ""
-
-#: src/webex/pages/withdraw.tsx:143
-#, c-format
-msgid "Select custom exchange"
-msgstr ""
-
-#: src/webex/pages/withdraw.tsx:163
-#, c-format
-msgid "You are about to withdraw %1$s from your bank account into your wallet."
-msgstr ""
-
-#: src/webex/pages/withdraw.tsx:174
-#, c-format
-msgid "Accept fees and withdraw"
-msgstr ""
-
-#: src/webex/pages/withdraw.tsx:192
-#, c-format
-msgid "Cancel withdraw operation"
-msgstr ""
-
-#: src/webex/renderHtml.tsx:249
-#, c-format
-msgid "Withdrawal fees:"
-msgstr ""
-
-#: src/webex/renderHtml.tsx:252
-#, c-format
-msgid "Rounding loss:"
-msgstr ""
-
-#: src/webex/renderHtml.tsx:254
-#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
-msgstr ""
-
-#: src/webex/renderHtml.tsx:262
-#, c-format
-msgid "# Coins"
-msgstr ""
-
-#: src/webex/renderHtml.tsx:263
-#, c-format
-msgid "Value"
-msgstr ""
-
-#: src/webex/renderHtml.tsx:264
-#, c-format
-msgid "Withdraw Fee"
-msgstr ""
-
-#: src/webex/renderHtml.tsx:265
-#, c-format
-msgid "Refresh Fee"
-msgstr ""
-
-#: src/webex/renderHtml.tsx:266
-#, c-format
-msgid "Deposit Fee"
-msgstr ""
-
-#, fuzzy
-#~ msgid "DEBUG: Your balance on %1$s is %2$s KUDO. Get more at %3$s"
-#~ msgstr "DEBUG: Your balance is %2$s KUDO on %1$s. Get more at %3$s"
diff --git a/packages/taler-wallet-webextension/src/i18n/es.po b/packages/taler-wallet-webextension/src/i18n/es.po
new file mode 100644
index 000000000..ea1fa9803
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/es.po
@@ -0,0 +1,2197 @@
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-07 07:03+0000\n"
+"Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n"
+"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/es/>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Saldo"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "Copia de seguridad"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr "Lector QR y Taler URI"
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Configuración"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr "Dev"
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr "OPERACIONES PENDIENTES"
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Cargando"
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr "No se pudo cargar los proveedores de copias de seguridad"
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr "No hay proveedores de copias de seguridad configurados"
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr "Agregar proveedor"
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr "Sincronizar todas las copias de seguridad"
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr "Sincronizar ahora"
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr "Ultima vez sincronizado"
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr "No sincronizado"
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr "Expira en"
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr "Hubo un error cargando los detalles del proveedor para \"%1$s\""
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr "No hay proveedor conocido con la URL \"%1$s\"."
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr "Ver proveedores"
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr "Última copia de seguridad"
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr "Copia de seguridad"
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr "Tarifa del proveedor"
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr "por año"
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr "Extender"
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms "
+"of service"
+msgstr ""
+"los términos han cambiado, extender el servicio implicará aceptar los nuevos "
+"términos de servicio"
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr "viejo"
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr "nuevo"
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr "tarifa"
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr "almacenamiento"
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr "Eliminar proveedor"
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr "Este proveedor ha reportado un error"
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr "Hay un conflicto con otra copia de seguridad de %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr "La copia de seguridad no es legible"
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr "Problema de copia de seguridad desconocido: %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr "servicio pagado"
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr "Copia de seguridad válida hasta"
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Abrir página de reserva"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Abrir página de pago"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Abrir página de devolución"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "Abrir página de propina"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Abrir página de retirada"
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr "Retirar dinero digital"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "No se pudo cargar la página"
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr "Agregar"
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr "Envíar %1$s"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr "Acción Taler"
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr "Esta página tiene una acción de pago."
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr "Esta página tiene una acción de retirada."
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr "Esta página tiene una acción de propina."
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr "Esta página tiene una acción de notificación de reserva."
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr "Notificar"
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr "Esta página tiene una acción de devolución."
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr "Esta página tiene una URI de Taler malformada."
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr "Descartar"
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr "Este popup está siendo cerrado y estás siendo redirigido a %1$s"
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr "No se pudo cargar el detalle de la propuesta"
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr "Id de orden"
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr "Resumen"
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr "Monto"
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr "Comerciante"
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr "Jurisdicción"
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr "Dirección del comerciante"
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr "Logo"
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr "Siti web"
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr "Correo electrónico"
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr "Clave pública"
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr "Fecha de entrega"
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr "Ubicación de entrega"
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr "Productos"
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr "Creado en"
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr "Plazo de reembolso"
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr "Devolución automática"
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr "Plazo de pago"
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr "URL de cumplimiento"
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr "Mensaje de éxito"
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr "Máxima comisión de depósito"
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr "Máxima comisión"
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr "Edad mínima"
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr "Amortización de comisión de transferencia"
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr "Auditores"
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr "Exchanges"
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr "Cuenta bancaria"
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr "Dirección de Bitcoin"
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr "IBAN"
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr "No se pudo cargar el estado del depósito"
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr "Depósito de dinero digital"
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr "Costo"
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr "Comisión"
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr "A recibir"
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr "Envíar %1$s"
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr "Detalle de transferencia Bitcoin"
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an "
+"minimum amount."
+msgstr ""
+"El exchange necesita una transacción con 3 salidas, una salida es hacia la "
+"cuenta del exchange y las otras dos son direcciones segwit falsas para "
+"metadata con el monto mínimo."
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two "
+"additional recipient and copy addresses and amounts"
+msgstr ""
+"En la billetera bitcoincore usar el botón \"Agregar destinatario\" para "
+"agregar dos destinatarios y copiar las direcciones y montos"
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid ""
+"Make sure the amount show %1$s BTC, else you have to change the base unit to "
+"BTC"
+msgstr ""
+"Asegurarse de que el monto muestre %1$s BTC, sino tendrá que cambiar la "
+"unidad a BTC"
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr "Cuenta"
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr "Banco anfitrión"
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr "Detalle de transferencia bancaria"
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr "Asunto"
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr "Nombre del receptor"
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr "No se pudo cargar información de la transacción"
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr "Hubo un error intentando completar la transacción"
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr "Esta transacción no está completada"
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr "Envíar"
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr "Reintentar"
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr "Olvidar"
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr "Cuidado!"
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to "
+"get the coins form it."
+msgstr ""
+"Si tú ya has transferido dinero al exchange, perderás la oportunidad de "
+"recibir las monedas desde este."
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr "Confirmar"
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr "Extracción"
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+"Asegúrate de usar el asunto correcto, de lo contrario el dinero no llegará a "
+"esta billetera."
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and "
+"check there is no pending step."
+msgstr ""
+"El banco todavía no confirmó la transferencia. Ir a %1$s %2$s y verificar "
+"que no hay pasos pendientes."
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid ""
+"Bank has confirmed the wire transfer. Waiting for the exchange to send the "
+"coins"
+msgstr ""
+"El banco confirmó la transferencia. Esperando que el exchange envíe las "
+"monedas"
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr "Detalles"
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr "Pago"
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr "Devoluciones"
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr "%1$s %2$s en %3$s"
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid ""
+"Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+"El comerciante creó una devolución para esta orden pero no fue recogida "
+"automáticamente."
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr "Oferta"
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr "Aceptar"
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr "Comerciante"
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr "Id de factura"
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr "Depósito"
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr "Actualizar"
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr "Propina"
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr "Devolución"
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr "Id de orden original"
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr "Resumen de compra"
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr "Copiar"
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr "Esconder QR"
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr "Mostrar QR"
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr "Crédito"
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr "Factura"
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr "Exchange"
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr "URI"
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr "Débito"
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr "Transferencia"
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr "País"
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr "Detalle de dirección"
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr "Número de edificio"
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr "Nombre de edificio"
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr "Calle"
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr "Código postal"
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr "Ubicación de ciudad"
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr "Ciudad"
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr "Distrito"
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr "Subdivisión de país"
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr "Fecha"
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr "Comisiones de transacción"
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr "Total"
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr "Retirar"
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr "Precio"
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr "Reembolsado"
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr "Entrega"
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr "Total transferido"
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr "No se pudo cargar el estado del pago"
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr "Pago con dinero digital"
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr "Compra"
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr "Recibo"
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr "Válido hasta"
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr "Lista de productos"
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr "Gratis"
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr "Ya pagado, estás siendo dirigido a %1$s"
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr "Ya pagado"
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr "Ya reclamado"
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr "Pagar con un teléfono móvil"
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr "Esconder QR"
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr "Escanear el código QR o %1$s"
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr "Pagar %1$s"
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr "No hay balance para esta divisa. Extraer dinero digital primero."
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+"No se encontraron suficientes monedas para pagar. Incluso si tuviera "
+"suficiente %1$s algunas restricciones se podrían aplicar."
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr "Tu balance no es suficiente."
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr "Mensaje del comerciante"
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr "No se pudo cargar el estado de la devolución"
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr "Devolución de dinero digital"
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr "Has ignorado la propina."
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr "El proceso de devolución está en progreso."
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr "Total para devolver"
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr "El comerciante \"%1$s\" te está ofreciendo una devolución."
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr "Monto de la orden"
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr "Ya devuelto"
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr "Devolución ofrecida"
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr "Aceptar %1$s"
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr "No se pudo cargar el estado de la propina"
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr "Propina con dinero digital"
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr "El comerciante te ofrece una propina"
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr "URL del comerciante"
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr "Recibir %1$s"
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+"Propina de %1$s aceptada. Revisa tu lista de transacciones para más detalle."
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr "Seleccione una opción"
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr "No se pudo cargar"
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr "Mostrar términos de servicio"
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr "Yo acepto los términos de servicio del exchange"
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr "El exchange no tiene los términos de servicio"
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr "Revisar los términos de servicio"
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr "Revisar los nuevos términos de servicio"
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr "El exchange respondió con unos términos de servicio vacíos"
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr "Descargar los términos de servicio"
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr "Esconder los términos de servicio"
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr "No se pudo cargar la comisión del exchange"
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr "Cerrar"
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr "No se pudo encontrar ningún exchange"
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr "No se pudo encontrar ningún exchange para la divisa %1$s"
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr "Descripción de comisión de servicio"
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr "Seleccionar exchange %1$s"
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr "Reiniciar"
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr "Usar este exchange"
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr "No tiene auditores"
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr "divisa"
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr "Operaciones"
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr "Depósitos"
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr "Operaciones"
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr "Hasta"
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr "Retiradas"
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr "Divisa"
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr "Operaciones de moneda"
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and "
+"is valid for a period of time. The exchange will charge the indicated amount "
+"every time a coin is used in such operation."
+msgstr ""
+"Toda operación en esta sección puede ser diferente por valor de denominación "
+"y es válida por un período. El exchange cobrará el monto indicado cada vez "
+"que una es usada en dicha operación."
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr "Operaciones de transferencia"
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is "
+"valid for a period of time. The exchange will charge the indicated amount "
+"every time a transfer is made."
+msgstr ""
+"Toda operación en esta sección puede ser diferente por tipo de transacción y "
+"es válida por un período. El exchange cobrará el monto indicado cada vez que "
+"se haga una transferencia."
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr "Operación"
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr "Operaciones de billetera"
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr "Característica"
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr "No se pudo obtener la información desde la URI"
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr "No se pudo obtener la información de retiro"
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr "Retirada de dinero digital"
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr "No se pudo completar la operación de retirada"
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr "Restricción etaria"
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr "Retirar %1$s"
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr "Retirar con un teléfono móvil"
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr "Factura digital"
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr "No se pudo completar la creación de la factura"
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr "Crear"
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr "No se pudo completar la operación de pago"
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr "Transferencia de dinero digital"
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr "No se pudo completar la operación de creación de transferencia"
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr "No se pudo completar la operación de recolección"
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr "Retirada Manual para %1$s"
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will "
+"send the coins to this wallet after receiving a wire transfer with the "
+"correct subject."
+msgstr ""
+"Elija un exchange desde donde las monedas serán retiradas. El exchange "
+"enviará las monedas a esta billetera después de recibir una transferencia "
+"bancaria con el asunto correcto."
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr "No se encontró exchange para %1$s"
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr "Agregar Exchange"
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr "Sin exchange configurado"
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr "No se pudo crear una reserva"
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr "Comenzar la retirada"
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr "No se pudo cargar el balance de depósito"
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr "Se debería especificar una divisa o un monto"
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr "No hay suficiente balance para hacer un depósito para la divisa %1$s"
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr "Enviar %1$s a tu cuenta"
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr "No hay una cuenta para hacer un depósito para la divisa %1$s"
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr "Agregar cuenta"
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr "Seleccionar cuenta"
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr "Agregar otra cuenta"
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr "Comisión de depósito"
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr "Depósito total"
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr "Depositar %1$s %2$s"
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr "Agregar cuenta de banco para %1$s"
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr "Ingresar la URL de un exchange en el que confíes."
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr "No fue posible agregar esta cuenta"
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr "Seleccione un tipo de cuenta"
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr "Revisar los términos de servicio"
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr "URL del Exchange"
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr "Agregar exchange"
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr "Agregar nuevo exchange"
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr "Agregar exchange para %1$s"
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+"Un exchange ha sido encontrado! Revisa la información y haz clic en siguiente"
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr "Este exchange no coincide con la divisa %1$s esperada"
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr "No fue posible verificar este exchange"
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr "No fue posible agregar este exchange"
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr "cargando"
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr "Versión"
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr "Siguiente"
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr "Esperando confirmación"
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr "PENDIENTE"
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr "No se pudo cargar la lista de transacciones"
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr "No hay historial para esta divisa."
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr "Agregar proveedor de copias de seguridad"
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr "No se pudo conseguir la información del proveedor"
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr "Los proveedores de copias de seguridad pueden cobrarte por su servicio"
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr "URL"
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr "Nombre"
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr "URL del proveedor"
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr "Por favor revisa y acepta los términos de servicio del proveedor"
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr "Precios"
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr "Gratis"
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr "%1$s por año de servicio"
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr "Alamcenamiento"
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr "%1$s megabytes de almacenamiento por año de servicio"
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr "Aceptar los términos de servicio"
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr "No se pudo obtener la información de la URI payto"
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr "Revisar la URI"
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr "El exchange está listo para la retirada"
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid ""
+"To complete the process you need to wire%1$s %2$s to the exchange bank "
+"account"
+msgstr ""
+"Para completar el proceso necesitas transferir %1$s %2$s a la cuenta "
+"bancaria del exchange"
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a "
+"banking app installed that supports RFC 8905"
+msgstr ""
+"Alternativamente, también puedes escanear el código QR o abrir %1$s si "
+"tienes una App bancaria instalada que soporta RFC 8905"
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr "Cancelar retirada"
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr "No se pudo cambiar el auto-open"
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr "No se pudo cambiar portapapeles"
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr "Navegador"
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr "Abrir automáticamente la billetera basada en el contenido de la página"
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser."
+msgstr ""
+"Habilitar la opción de debajo, hará que el uso de la billetera sea mas "
+"rápido, pero requiere más permisos de tu navegador."
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr "Revisar el portapapeles automáticamente por Taler URI"
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr "Confianza"
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr "No hay exchanges todavía"
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr "Términos de servicio"
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr "ok"
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr "modificado"
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr "no aceptado"
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr "desconocido (el estado del exchange debería actualizarse)"
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr "Agregar un exchange"
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr "Solución de problemas"
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr "Modo desarrollador"
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr "Más información y opciones útiles para depuración"
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr "Pantalla"
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr "Lenguaje actual"
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr "Wallet core"
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr "Web Extension"
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr "Compatibilidad con Exchange"
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr "Compatibilidad con Merchant"
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr "Compatibilidad con Bank"
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr "Extensión del navegador instalada!"
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr "Puedes abrir GNU Taler Wallet usando la combinación %1$s."
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+"También fijando GNU Taler Wallet a to navegador Chrome permite un acceso "
+"rápido sin el teclado:"
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr "Haz click en el ícono de rompecabezas"
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr "Busca \"GNU Taler Wallet\""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr "Haz click en el ícono de fijar"
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr "Permisos"
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser.)"
+msgstr ""
+"(Habilitar esta opción de abajo hará el uso de la billetera mas rápido, pero "
+"requiere mas permisos de tu navegador)"
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr "Próximos pasos"
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr "Probar la demostración"
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr "Aprender como llenar tu billetera"
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr "El diagnóstico caducó. No nos pudimos comunicar con la billetera."
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr "Problemas detectados:"
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check "
+"the preference name %2$s)."
+msgstr ""
+"Por favor revisa en tu configuración %1$s que tienes IndexedDB habilitado ("
+"el nombre de la preferencia %2$s)."
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+"La base de datos de la billetera expiró. Por ahora la migración automática "
+"no está soportada. Por favor dirijasé a %1$s para reiniciar la base de datos "
+"de la billetera."
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr "Ejecutando diagnósticos"
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr "Herramientas de desarrollo"
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE "
+"ALL YOUR COINS?"
+msgstr ""
+"Quieres DESTRUIR IRREVOCABLEMENTE todo dentro de tu billetera y PERDER TODAS "
+"TUS MONEDAS?"
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr "Reiniciar"
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr "TESTING: Esto puede borrar todas tus monedas, proceder con precaución"
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr "Ejecutar GC"
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr "importar base de datos"
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr "exportar base de datos"
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr "Base de datos exportada a %1$s %2$s para descargar"
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr "Monedas"
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr "Operaciones pendientes"
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr "monedas usables"
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr "id"
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr "denominación"
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr "valor"
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr "estado"
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr "desde refresco?"
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr "cantidad de age key"
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr "monedas gastadas"
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr "hacer clic para mostrar"
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr "Escanear un código QR o ingresar taler:// URI debajo"
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Abierto"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr "El URI no es válido. Taler URI debería comenzar con `taler://`"
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr "Intentar otro"
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr "No se pudo cargar la lista de exchange"
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr "Elija una divisa para proceder o agregue otro exchange"
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr "Divisas conocidas"
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr "Indicar el monto y el origen"
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr "Cambiar divisa"
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr "Usar un origen previo:"
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr "O especificar el origen del dinero"
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr "Especificar el origen del dinero"
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr "Desde mi cuenta de banco"
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr "Desde otra billetera"
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr "Divisa no provista"
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr "Especificar el monto y el destino"
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr "Usar destinos previos:"
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr "O especificar el destino del dinero"
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr "Especificar el destino del dinero"
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr "Hacia mi cuenta de banco"
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr "Hacia otra billetera"
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr "No se pudo cargar la información de recuperación de copia de seguridad"
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr "Recuperación de billetera digital"
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr "Importar copia de seguridad, mostrar información"
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr "Todo completo, su transacción está en progreso"
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr "Editar"
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr "No se pudo cargar la lista de exchange conocidos"
+
+#~ msgid "Back"
+#~ msgstr "Atrás"
+
+#~ msgid "You have no balance to show."
+#~ msgstr "No tienes balance para mostrar."
+
+#~ msgid ""
+#~ "To withdraw money you can start from your bank site or click the "
+#~ "\"withdraw\" button to use a known exchange."
+#~ msgstr ""
+#~ "Para retirar dinero puedes empezar desde el sitio de tu banco o cliquear "
+#~ "en el botón \"retirar\" para usar un exchange conocido."
+
+#~ msgid "Deposit %1$s"
+#~ msgstr "Depositar %1$s"
+
+#~ msgid "Enter URI"
+#~ msgstr "Ingresar URI"
+
+#~ msgid "Loading terms.."
+#~ msgstr "Cargando términos..."
+
+#~ msgid "Add exchange anyway"
+#~ msgstr "Agregar exchange de todas maneras"
+
+#~ msgid "back"
+#~ msgstr "volver"
+
+#~ msgid "no balance"
+#~ msgstr "sin balance"
+
+#~ msgid "There is no known bank account to send money to"
+#~ msgstr "No hay una cuenta bancaria conocida, donde enviar el dinero"
+
+#~ msgid "Bank account IBAN number"
+#~ msgstr "Número IBAN de cuenta bancaria"
+
+#~ msgid "Chosen amount"
+#~ msgstr "Elegir cantidad"
+
+#~ msgid "could not parse payto uri from exchange %1$s"
+#~ msgstr "No se pudo analizar la URI pagar-a del exchange %1$s"
+
+#~ msgid "Exchange fee"
+#~ msgstr "Comisión del exchange"
+
+#~ msgid "The bank is waiting for confirmation. Go to the %1$s"
+#~ msgstr "El banco espera la confirmación. Dirigete a %1$s"
+
+#~ msgid "Waiting for the coins to arrive"
+#~ msgstr "Esperando a que las monedas lleguen"
+
+#~ msgid "Total paid"
+#~ msgstr "Total pagado"
+
+#~ msgid "Purchase amount"
+#~ msgstr "Importe de la compra"
+
+#~ msgid "Total send"
+#~ msgstr "Total enviado"
+
+#~ msgid "Deposit amount"
+#~ msgstr "Cantidad a depositar"
+
+#~ msgid "Total refresh"
+#~ msgstr "Actualización total"
+
+#~ msgid "Total tip"
+#~ msgstr "Total de propina"
+
+#~ msgid "Thank you for installing the wallet."
+#~ msgstr "Gracias por haber instalado la billetera."
+
+#~ msgid "Could not load contract terms from merchant or wallet backend."
+#~ msgstr ""
+#~ "No se pudieron cargar los términos de contrato del comerciante o de la "
+#~ "billetera."
+
+#~ msgid "Processing"
+#~ msgstr "Procesando"
+
+#~ msgid "Your balance of %1$s is not enough to pay for this purchase"
+#~ msgstr "Tu balance de %1$s no es suficiente para pagar por esta compra"
+
+#~ msgid "Payment complete"
+#~ msgstr "Pago completado"
+
+#~ msgid "You are going to be redirected to $ %1$s"
+#~ msgstr "Vas a ser redirigido a %1$s"
+
+#~ msgid "You can close this page."
+#~ msgstr "Puedes cerrar esta página."
+
+#~ msgid "Total to pay"
+#~ msgstr "Total a pagar"
+
+#~ msgid "Refund Status"
+#~ msgstr "Estado del reembolso"
+
+#~ msgid "The product %1$s has received a total effective refund of"
+#~ msgstr "El producto %1$s ha recibido un reembolso total efectivo de"
+
+#~ msgid "The refund amount of %1$s could not be applied."
+#~ msgstr "El importe del reembolso de %1$s no pudo ser aplicado."
+
+#~ msgid "missing taler refund uri"
+#~ msgstr "falta la URI Taler de reembolso"
+
+#~ msgid "Error: %1$s"
+#~ msgstr "Error: %1$s"
+
+#~ msgid "Updating refund status"
+#~ msgstr "Actualizando el estado de reembolso"
+
+#~ msgid "Ignore"
+#~ msgstr "Ignorar"
+
+#~ msgid "missing tip uri"
+#~ msgstr "falta la URI de la propina"
+
+#~ msgid "Total to withdraw"
+#~ msgstr "Total a retirar"
+
+#~ msgid "Cancel exchange selection"
+#~ msgstr "Cancelar la selección de exchange"
+
+#~ msgid "Confirm exchange selection"
+#~ msgstr "Confirmar la selección de exchange"
+
+#~ msgid "Confirm withdrawal"
+#~ msgstr "Confirmar retirada"
+
+#~ msgid "Withdraw anyway"
+#~ msgstr "Retirar de todas maneras"
+
+#~ msgid "missing withdraw uri"
+#~ msgstr "falta la URI de retirada"
+
+#~ msgid "missing pay uri"
+#~ msgstr "falta la URI de pago"
+
+#~ msgid "Could not get the payment information for this order"
+#~ msgstr "No se pudo obtener la información de pago para esta orden"
+
+#~ msgid "Loading payment information"
+#~ msgstr "Cargado la información de pago"
+
+#~ msgid "You will now be sent back to the merchant you came from."
+#~ msgstr "Ahora serás enviado de nuevo al comerciante desde donde viniste."
+
+#~ msgid "Manual Reset Required"
+#~ msgstr "Reinicio Manual Necesario"
+
+#~ msgid ""
+#~ "The wallet&apos;s database in your browser is incompatible with the "
+#~ "currently installed wallet. Please reset manually."
+#~ msgstr ""
+#~ "La base de datos de billetera en tu navegador es incompatible con la "
+#~ "billetera instalada actualmente. Por favor reinicie manualmente."
+
+#~ msgid ""
+#~ "Once the database format has stabilized, we will provide automatic "
+#~ "upgrades."
+#~ msgstr ""
+#~ "Una vez que el formato de la base de datos se haya estabilizado, "
+#~ "proveeremos de actualizaciones automáticas."
+
+#~ msgid "I understand that I will lose all my data"
+#~ msgstr "Entiendo que perderé toda mi información"
+
+#~ msgid "Everything is fine!"
+#~ msgstr "Todo está bien!"
+
+#~ msgid "A reset is not required anymore, you can close this page."
+#~ msgstr "Un reinicio ya no es necesario, puede cerrar esta página."
+
+#~ msgid "Not implemented yet."
+#~ msgstr "Todavía no implementado."
diff --git a/packages/taler-wallet-webextension/src/i18n/fi.po b/packages/taler-wallet-webextension/src/i18n/fi.po
new file mode 100644
index 000000000..c6196b7f3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/fi.po
@@ -0,0 +1,1967 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-20 00:10+0000\n"
+"Last-Translator: Sara Korpinen <sara.a.korpinen@gmail.com>\n"
+"Language-Team: Finnish <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/fi/>\n"
+"Language: fi\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Saldo"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "Varmuuskopio"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr "QR -lukija ja Taler URI"
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Asetukset"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr "Kehitys"
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr "ODOTTAVAT TOIMINNOT"
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Lataa"
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr "Varmuuskopion tarjoajia ei voitu ladata"
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr "Varmuuskopion tarjoajia ei ole määritetty"
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr "Lisää palveluntarjoaja"
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr "Synkronoi kaikki varmuuskopiot"
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr "Synkronoi nyt"
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr "Viimeksi synkronoitu"
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr "Ei synkronoitu"
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr "Vanhenee"
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr "Virhe ladattaessa palveluntarjoajan tietoja kohteelle &quot; %1$s&quot;"
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr "Ei tunneta palveluntarjoajaa, jonka URL-osoite on &quot;%1$s&quot;."
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr "Katso palveluntarjoajat"
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr "Viimeisin varmuuskopio"
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr "Varmuuskopioi"
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr "Palveluntarjoajan maksu"
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr "vuodessa"
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr "Laajenna"
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
+msgstr ""
+"ehdot ovat muuttuneet, palvelun laajentaminen tarkoittaa uusien "
+"käyttöehtojen hyväksymistä"
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr "vanha"
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr "uusi"
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr "maksu"
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr "tila"
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr "Poista palveluntarjoaja"
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr "Tämä palveluntarjoaja on ilmoittanut virheestä"
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr "Ristiriita toisen varmuuskopion kanssa kohteesta %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr "Varmuuskopiota ei voi lukea"
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr "Tuntematon varmuuskopiointi ongelma: %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr "palvelu maksettu"
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format, fuzzy
+msgid "Backup valid until"
+msgstr "Varmuuskopio voimassa"
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Peruuta"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Avaa varaussivu"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Avaa maksusivu"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Avaa hyvityssivu"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "Avaa tippi sivu"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Avaa nostosivu"
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr "Hanki digitaalista käteistä"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "Ei voitu ladata saldosivua"
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr "Lisää"
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr "Lähetä %1$s"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr "Taler toiminta"
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr "Tällä sivulla on maksutoiminto."
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr "Tällä sivulla on nosto toiminto."
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr "Tällä sivulla on tippaus toiminto."
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr "Tällä sivulla on ilmoitus varaus toiminto."
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr "Ilmoita"
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr "Tällä sivulla on hyvitys toiminto."
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr "Tällä sivulla on väärin muotoiltu taler uri."
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr "Hylkää"
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr "tämä ponnahdusikkuna suljetaan ja sinut ohjataan osoitteeseen %1$s"
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr "Ostoehdotuksen tietoja ei voitu ladata"
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr "Tilausnumero"
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr "Yhteenveto"
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr "Summa"
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr "Kauppiaan nimi"
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr "Kauppiaan toimivalta"
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr "Kauppiaan osoite"
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr "Kauppiaan logo"
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr "Kauppiaan nettisivut"
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr "Kauppiaan sähköposti"
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr "Kauppiaan julkinen avain"
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr "Toimituspäivä"
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr "Toimituspaikka"
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr "Tuotteet"
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr "Luotu"
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr "Palautuksen määräaika"
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr "Automaattinen palautus"
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr "Maksun määräaika"
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr "Toteutus-URL"
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr "Toteutusviesti"
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr "Max talletusmaksu"
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr "Max maksu"
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr "Alaikäraja"
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr "Pankkimaksun lyhennys"
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr "Tilintarkastajat"
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr "Vaihdot"
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr "Pankkitili"
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr "Bitcoin osoite"
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr "IBAN"
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr "Talletuksen tilaa ei voitu ladata"
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr "Digitaalinen käteistalletus"
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr "Kustannus"
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr "Maksu"
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr "Vastaanotettava"
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr "Lähetä &nbsp; %1$s"
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr "Bitcoin -siirron tiedot"
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+"Pörssi tarvitsee tapahtuman, jossa on 3 lähtöä, joista yksi on vaihtotili ja "
+"kaksi muuta ovat segwit fake -osoitteita metatiedoille vähimmäismäärällä."
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+"Käytä bitcoincore-lompakossa &apos;Lisää vastaanottaja&apos; -painiketta "
+"lisätäksesi kaksi muuta vastaanottajaa ja kopioidaksesi osoitteet ja summat"
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+"Varmista, että summa näyttää %1$s BTC, muuten sinun on vaihdettava "
+"perusyksikkö BTC:ksi"
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr "Tili"
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr "Pankin isäntä"
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr "Pankkisiirtotiedot"
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr "Aihe"
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr "Vastaanottajan nimi"
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr "Tapahtumatietoja ei voitu ladata"
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr "Tapahtuman suorittamisessa tapahtui virhe"
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr "Tätä tapahtumaa ei ole suoritettu loppuun"
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr "Lähetä"
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr "Yritä uudelleen"
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr "Unohda"
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr "Varoitus!"
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+"Jos olet jo siirtänyt rahaa vaihtoon, menetät mahdollisuuden saada kolikot "
+"siitä."
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr "Vahvista"
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr "Nosto"
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+"Varmista, että käytät oikeaa aihetta, muuten rahat eivät tule tähän "
+"lompakkoon."
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+"Pankki ei ole vielä vahvistanut pankkisiirtoa. Siirry kohtaan %1$s %2$s ja "
+"tarkista, ettei odottavaa vaihetta ole."
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Avoin"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
diff --git a/packages/taler-wallet-webextension/src/i18n/fr.po b/packages/taler-wallet-webextension/src/i18n/fr.po
index 67b09de1a..462eb30f7 100644
--- a/packages/taler-wallet-webextension/src/i18n/fr.po
+++ b/packages/taler-wallet-webextension/src/i18n/fr.po
@@ -1,290 +1,1969 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
#
-# TALER is free software; you can redistribute it and/or modify it under the
+# GNU Taler is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3, or (at your option) any later version.
#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"PO-Revision-Date: 2024-02-28 08:07+0000\n"
+"Last-Translator: d0p1 <contact@d0p1.eu>\n"
+"Language-Team: French <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/fr/>\n"
+"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
+"X-Generator: Weblate 5.2.1\n"
-#: src/util/wire.ts:37
+#: src/NavigationBar.tsx:139
#, c-format
-msgid "Invalid Wire"
+msgid "Balance"
msgstr ""
-#: src/util/wire.ts:42 src/util/wire.ts:45
+#: src/NavigationBar.tsx:142
#, c-format
-msgid "Invalid Test Wire Detail"
+msgid "Backup"
msgstr ""
-#: src/util/wire.ts:47
+#: src/NavigationBar.tsx:147
#, c-format
-msgid "Test Wire Acct #%1$s on %2$s"
+msgid "QR Reader and Taler URI"
msgstr ""
-#: src/util/wire.ts:49
+#: src/NavigationBar.tsx:154
#, c-format
-msgid "Unknown Wire Detail"
+msgid "Settings"
msgstr ""
-#: src/webex/pages/benchmark.tsx:52
+#: src/NavigationBar.tsx:184
#, c-format
-msgid "Operation"
+msgid "Dev"
msgstr ""
-#: src/webex/pages/benchmark.tsx:53
+#: src/mui/Typography.tsx:122
#, c-format
-msgid "time (ms/op)"
+msgid "%1$s"
msgstr ""
-#: src/webex/pages/pay.tsx:130
+#: src/components/PendingTransactions.tsx:74
#, c-format
-msgid "The merchant %1$s offers you to purchase:"
+msgid "PENDING OPERATIONS"
msgstr ""
-#: src/webex/pages/pay.tsx:136
+#: src/components/Loading.tsx:36
#, c-format
-msgid "The total price is %1$s (plus %2$s fees)."
+msgid "Loading"
msgstr ""
-#: src/webex/pages/pay.tsx:141
+#: src/wallet/BackupPage.tsx:123
#, c-format
-msgid "The total price is %1$s."
+msgid "Could not load backup providers"
msgstr ""
-#: src/webex/pages/pay.tsx:163
+#: src/wallet/BackupPage.tsx:202
#, c-format
-msgid "Retry"
+msgid "No backup providers configured"
msgstr ""
-#: src/webex/pages/pay.tsx:173
+#: src/wallet/BackupPage.tsx:205
#, c-format
-msgid "Confirm payment"
+msgid "Add provider"
msgstr ""
-#: src/webex/pages/popup.tsx:153
+#: src/wallet/BackupPage.tsx:219
#, c-format
-msgid "Balance"
+msgid "Sync all backups"
msgstr ""
-#: src/webex/pages/popup.tsx:154
+#: src/wallet/BackupPage.tsx:221
#, c-format
-msgid "History"
+msgid "Sync now"
msgstr ""
-#: src/webex/pages/popup.tsx:155
+#: src/wallet/BackupPage.tsx:264
#, c-format
-msgid "Debug"
+msgid "Last synced"
msgstr ""
-#: src/webex/pages/popup.tsx:175
+#: src/wallet/BackupPage.tsx:269
#, c-format
-msgid "You have no balance to show. Need some %1$s getting started?"
+msgid "Not synced"
msgstr ""
-#: src/webex/pages/popup.tsx:238
+#: src/wallet/BackupPage.tsx:289
#, c-format
-msgid "%1$s incoming"
+msgid "Expires in"
msgstr ""
-#: src/webex/pages/popup.tsx:250
+#: src/wallet/ProviderDetailPage.tsx:60
#, c-format
-msgid "%1$s being spent"
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
msgstr ""
-#: src/webex/pages/popup.tsx:281
+#: src/wallet/ProviderDetailPage.tsx:108
#, c-format
-msgid "Error: could not retrieve balance information."
+msgid "There is not known provider with url &quot;%1$s&quot;."
msgstr ""
-#: src/webex/pages/popup.tsx:390
+#: src/wallet/ProviderDetailPage.tsx:115
#, c-format
-msgid "Invalid "
+msgid "See providers"
msgstr ""
-#: src/webex/pages/popup.tsx:396
+#: src/wallet/ProviderDetailPage.tsx:143
#, c-format
-msgid "Fees "
+msgid "Last backup"
msgstr ""
-#: src/webex/pages/popup.tsx:434
+#: src/wallet/ProviderDetailPage.tsx:148
#, c-format
-msgid "Refresh sessions has completed"
+msgid "Back up"
msgstr ""
-#: src/webex/pages/popup.tsx:451
+#: src/wallet/ProviderDetailPage.tsx:154
#, c-format
-msgid "Order Refused"
+msgid "Provider fee"
msgstr ""
-#: src/webex/pages/popup.tsx:465
+#: src/wallet/ProviderDetailPage.tsx:157
#, c-format
-msgid "Order redirected"
+msgid "per year"
msgstr ""
-#: src/webex/pages/popup.tsx:482
+#: src/wallet/ProviderDetailPage.tsx:163
#, c-format
-msgid "Payment aborted"
+msgid "Extend"
msgstr ""
-#: src/webex/pages/popup.tsx:512
+#: src/wallet/ProviderDetailPage.tsx:169
#, c-format
-msgid "Payment Sent"
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms "
+"of service"
msgstr ""
-#: src/webex/pages/popup.tsx:536
+#: src/wallet/ProviderDetailPage.tsx:179
#, c-format
-msgid "Order accepted"
+msgid "old"
msgstr ""
-#: src/webex/pages/popup.tsx:547
+#: src/wallet/ProviderDetailPage.tsx:183
#, c-format
-msgid "Reserve balance updated"
+msgid "new"
msgstr ""
-#: src/webex/pages/popup.tsx:559
+#: src/wallet/ProviderDetailPage.tsx:190
#, c-format
-msgid "Payment refund"
+msgid "fee"
msgstr ""
-#: src/webex/pages/popup.tsx:584
+#: src/wallet/ProviderDetailPage.tsx:198
#, c-format
-msgid "Withdrawn"
+msgid "storage"
msgstr ""
-#: src/webex/pages/popup.tsx:596
+#: src/wallet/ProviderDetailPage.tsx:215
#, c-format
-msgid "Tip Accepted"
+msgid "Remove provider"
msgstr ""
-#: src/webex/pages/popup.tsx:606
+#: src/wallet/ProviderDetailPage.tsx:228
#, c-format
-msgid "Tip Declined"
+msgid "This provider has reported an error"
msgstr ""
-#: src/webex/pages/popup.tsx:615
+#: src/wallet/ProviderDetailPage.tsx:242
#, c-format
-msgid "%1$s"
+msgid "There is conflict with another backup from %1$s"
msgstr ""
-#: src/webex/pages/popup.tsx:707
+#: src/wallet/ProviderDetailPage.tsx:253
#, c-format
-msgid "Your wallet has no events recorded."
+msgid "Backup is not readable"
msgstr ""
-#: src/webex/pages/return-coins.tsx:124
+#: src/wallet/ProviderDetailPage.tsx:261
#, c-format
-msgid "Wire to bank account"
+msgid "Unknown backup problem: %1$s"
msgstr ""
-#: src/webex/pages/return-coins.tsx:206
+#: src/wallet/ProviderDetailPage.tsx:283
#, c-format
-msgid "Confirm"
+msgid "service paid"
msgstr ""
-#: src/webex/pages/return-coins.tsx:209
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:57
#, c-format
msgid "Cancel"
+msgstr "Annuler"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr ""
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr ""
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an "
+"minimum amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two "
+"additional recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid ""
+"Make sure the amount show %1$s BTC, else you have to change the base unit to "
+"BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to "
+"get the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr "Confirmer"
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and "
+"check there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid ""
+"Bank has confirmed the wire transfer. Waiting for the exchange to send the "
+"coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
msgstr ""
-#: src/webex/pages/withdraw.tsx:73
+#: src/wallet/Transaction.tsx:378
#, c-format
-msgid "Could not get details for withdraw operation:"
+msgid "Refunds"
msgstr ""
-#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#: src/wallet/Transaction.tsx:385
#, c-format
-msgid "Chose different exchange provider"
+msgid "%1$s %2$s on %3$s"
msgstr ""
-#: src/webex/pages/withdraw.tsx:109
+#: src/wallet/Transaction.tsx:415
#, c-format
msgid ""
-"Please select an exchange. You can review the details before after your "
-"selection."
+"Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr "Exchange"
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr "Fermer"
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and "
+"is valid for a period of time. The exchange will charge the indicated amount "
+"every time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is "
+"valid for a period of time. The exchange will charge the indicated amount "
+"every time a transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will "
+"send the coins to this wallet after receiving a wire transfer with the "
+"correct subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid ""
+"To complete the process you need to wire%1$s %2$s to the exchange bank "
+"account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a "
+"banking app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check "
+"the preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE "
+"ALL YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
msgstr ""
-#: src/webex/pages/withdraw.tsx:121
+#: src/wallet/DestinationSelection.tsx:449
#, c-format
-msgid "Select %1$s"
+msgid "currency not provided"
msgstr ""
-#: src/webex/pages/withdraw.tsx:143
+#: src/wallet/DestinationSelection.tsx:459
#, c-format
-msgid "Select custom exchange"
+msgid "Specify the amount and the destination"
msgstr ""
-#: src/webex/pages/withdraw.tsx:163
+#: src/wallet/DestinationSelection.tsx:483
#, c-format
-msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgid "Use previous destinations:"
msgstr ""
-#: src/webex/pages/withdraw.tsx:174
+#: src/wallet/DestinationSelection.tsx:503
#, c-format
-msgid "Accept fees and withdraw"
+msgid "Or specify the destination of the money"
msgstr ""
-#: src/webex/pages/withdraw.tsx:192
+#: src/wallet/DestinationSelection.tsx:511
#, c-format
-msgid "Cancel withdraw operation"
+msgid "Specify the destination of the money"
msgstr ""
-#: src/webex/renderHtml.tsx:249
+#: src/wallet/DestinationSelection.tsx:521
#, c-format
-msgid "Withdrawal fees:"
+msgid "To my bank account"
msgstr ""
-#: src/webex/renderHtml.tsx:252
+#: src/wallet/DestinationSelection.tsx:534
#, c-format
-msgid "Rounding loss:"
+msgid "To another wallet"
msgstr ""
-#: src/webex/renderHtml.tsx:254
+#: src/cta/Recovery/views.tsx:30
#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
+msgid "Could not load backup recovery information"
msgstr ""
-#: src/webex/renderHtml.tsx:262
+#: src/cta/Recovery/views.tsx:47
#, c-format
-msgid "# Coins"
+msgid "Digital wallet recovery"
msgstr ""
-#: src/webex/renderHtml.tsx:263
+#: src/cta/Recovery/views.tsx:52
#, c-format
-msgid "Value"
+msgid "Import backup, show info"
msgstr ""
-#: src/webex/renderHtml.tsx:264
+#: src/wallet/Application.tsx:189
#, c-format
-msgid "Withdraw Fee"
+msgid "All done, your transaction is in progress"
msgstr ""
-#: src/webex/renderHtml.tsx:265
+#: src/components/EditableText.tsx:45
#, c-format
-msgid "Refresh Fee"
+msgid "Edit"
msgstr ""
-#: src/webex/renderHtml.tsx:266
+#: src/wallet/ManualWithdrawPage.tsx:102
#, c-format
-msgid "Deposit Fee"
+msgid "Could not load the list of known exchanges"
msgstr ""
diff --git a/packages/taler-wallet-webextension/src/i18n/it.po b/packages/taler-wallet-webextension/src/i18n/it.po
index 67b09de1a..e0568b8f8 100644
--- a/packages/taler-wallet-webextension/src/i18n/it.po
+++ b/packages/taler-wallet-webextension/src/i18n/it.po
@@ -1,290 +1,1969 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
#
-# TALER is free software; you can redistribute it and/or modify it under the
+# GNU Taler is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3, or (at your option) any later version.
#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"PO-Revision-Date: 2023-08-16 12:43+0000\n"
+"Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
+"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/it/>\n"
+"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
-#: src/util/wire.ts:37
+#: src/NavigationBar.tsx:139
#, c-format
-msgid "Invalid Wire"
+msgid "Balance"
+msgstr "Saldo"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr ""
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr "Lettore QR e Taler URI"
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Impostazioni"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
msgstr ""
-#: src/util/wire.ts:42 src/util/wire.ts:45
+#: src/mui/Typography.tsx:122
#, c-format
-msgid "Invalid Test Wire Detail"
+msgid "%1$s"
msgstr ""
-#: src/util/wire.ts:47
+#: src/components/PendingTransactions.tsx:74
#, c-format
-msgid "Test Wire Acct #%1$s on %2$s"
+msgid "PENDING OPERATIONS"
msgstr ""
-#: src/util/wire.ts:49
+#: src/components/Loading.tsx:36
#, c-format
-msgid "Unknown Wire Detail"
+msgid "Loading"
msgstr ""
-#: src/webex/pages/benchmark.tsx:52
+#: src/wallet/BackupPage.tsx:123
#, c-format
-msgid "Operation"
+msgid "Could not load backup providers"
msgstr ""
-#: src/webex/pages/benchmark.tsx:53
+#: src/wallet/BackupPage.tsx:202
#, c-format
-msgid "time (ms/op)"
+msgid "No backup providers configured"
msgstr ""
-#: src/webex/pages/pay.tsx:130
+#: src/wallet/BackupPage.tsx:205
#, c-format
-msgid "The merchant %1$s offers you to purchase:"
+msgid "Add provider"
msgstr ""
-#: src/webex/pages/pay.tsx:136
+#: src/wallet/BackupPage.tsx:219
#, c-format
-msgid "The total price is %1$s (plus %2$s fees)."
+msgid "Sync all backups"
msgstr ""
-#: src/webex/pages/pay.tsx:141
+#: src/wallet/BackupPage.tsx:221
#, c-format
-msgid "The total price is %1$s."
+msgid "Sync now"
msgstr ""
-#: src/webex/pages/pay.tsx:163
+#: src/wallet/BackupPage.tsx:264
#, c-format
-msgid "Retry"
+msgid "Last synced"
msgstr ""
-#: src/webex/pages/pay.tsx:173
+#: src/wallet/BackupPage.tsx:269
#, c-format
-msgid "Confirm payment"
+msgid "Not synced"
msgstr ""
-#: src/webex/pages/popup.tsx:153
+#: src/wallet/BackupPage.tsx:289
#, c-format
-msgid "Balance"
+msgid "Expires in"
msgstr ""
-#: src/webex/pages/popup.tsx:154
+#: src/wallet/ProviderDetailPage.tsx:60
#, c-format
-msgid "History"
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
msgstr ""
-#: src/webex/pages/popup.tsx:155
+#: src/wallet/ProviderDetailPage.tsx:108
#, c-format
-msgid "Debug"
+msgid "There is not known provider with url &quot;%1$s&quot;."
msgstr ""
-#: src/webex/pages/popup.tsx:175
+#: src/wallet/ProviderDetailPage.tsx:115
#, c-format
-msgid "You have no balance to show. Need some %1$s getting started?"
+msgid "See providers"
msgstr ""
-#: src/webex/pages/popup.tsx:238
+#: src/wallet/ProviderDetailPage.tsx:143
#, c-format
-msgid "%1$s incoming"
+msgid "Last backup"
msgstr ""
-#: src/webex/pages/popup.tsx:250
+#: src/wallet/ProviderDetailPage.tsx:148
#, c-format
-msgid "%1$s being spent"
+msgid "Back up"
msgstr ""
-#: src/webex/pages/popup.tsx:281
+#: src/wallet/ProviderDetailPage.tsx:154
#, c-format
-msgid "Error: could not retrieve balance information."
+msgid "Provider fee"
msgstr ""
-#: src/webex/pages/popup.tsx:390
+#: src/wallet/ProviderDetailPage.tsx:157
#, c-format
-msgid "Invalid "
+msgid "per year"
msgstr ""
-#: src/webex/pages/popup.tsx:396
+#: src/wallet/ProviderDetailPage.tsx:163
#, c-format
-msgid "Fees "
+msgid "Extend"
msgstr ""
-#: src/webex/pages/popup.tsx:434
+#: src/wallet/ProviderDetailPage.tsx:169
#, c-format
-msgid "Refresh sessions has completed"
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms "
+"of service"
msgstr ""
-#: src/webex/pages/popup.tsx:451
+#: src/wallet/ProviderDetailPage.tsx:179
#, c-format
-msgid "Order Refused"
+msgid "old"
msgstr ""
-#: src/webex/pages/popup.tsx:465
+#: src/wallet/ProviderDetailPage.tsx:183
#, c-format
-msgid "Order redirected"
+msgid "new"
msgstr ""
-#: src/webex/pages/popup.tsx:482
+#: src/wallet/ProviderDetailPage.tsx:190
#, c-format
-msgid "Payment aborted"
+msgid "fee"
msgstr ""
-#: src/webex/pages/popup.tsx:512
+#: src/wallet/ProviderDetailPage.tsx:198
#, c-format
-msgid "Payment Sent"
+msgid "storage"
msgstr ""
-#: src/webex/pages/popup.tsx:536
+#: src/wallet/ProviderDetailPage.tsx:215
#, c-format
-msgid "Order accepted"
+msgid "Remove provider"
msgstr ""
-#: src/webex/pages/popup.tsx:547
+#: src/wallet/ProviderDetailPage.tsx:228
#, c-format
-msgid "Reserve balance updated"
+msgid "This provider has reported an error"
msgstr ""
-#: src/webex/pages/popup.tsx:559
+#: src/wallet/ProviderDetailPage.tsx:242
#, c-format
-msgid "Payment refund"
+msgid "There is conflict with another backup from %1$s"
msgstr ""
-#: src/webex/pages/popup.tsx:584
+#: src/wallet/ProviderDetailPage.tsx:253
#, c-format
-msgid "Withdrawn"
+msgid "Backup is not readable"
msgstr ""
-#: src/webex/pages/popup.tsx:596
+#: src/wallet/ProviderDetailPage.tsx:261
#, c-format
-msgid "Tip Accepted"
+msgid "Unknown backup problem: %1$s"
msgstr ""
-#: src/webex/pages/popup.tsx:606
+#: src/wallet/ProviderDetailPage.tsx:283
#, c-format
-msgid "Tip Declined"
+msgid "service paid"
msgstr ""
-#: src/webex/pages/popup.tsx:615
+#: src/wallet/ProviderDetailPage.tsx:290
#, c-format
-msgid "%1$s"
+msgid "Backup valid until"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr ""
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr "Importo"
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr ""
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
msgstr ""
-#: src/webex/pages/popup.tsx:707
+#: src/components/Part.tsx:160
#, c-format
-msgid "Your wallet has no events recorded."
+msgid "Bitcoin address"
msgstr ""
-#: src/webex/pages/return-coins.tsx:124
+#: src/components/Part.tsx:163
#, c-format
-msgid "Wire to bank account"
+msgid "IBAN"
msgstr ""
-#: src/webex/pages/return-coins.tsx:206
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an "
+"minimum amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two "
+"additional recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid ""
+"Make sure the amount show %1$s BTC, else you have to change the base unit to "
+"BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr "Soggetto"
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to "
+"get the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
#, c-format
msgid "Confirm"
+msgstr "Confermare"
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
msgstr ""
-#: src/webex/pages/return-coins.tsx:209
+#: src/wallet/Transaction.tsx:286
#, c-format
-msgid "Cancel"
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and "
+"check there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid ""
+"Bank has confirmed the wire transfer. Waiting for the exchange to send the "
+"coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid ""
+"Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr "Cambio"
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr "Data"
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr "Prelevare"
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr "Rimborsato"
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
msgstr ""
-#: src/webex/pages/withdraw.tsx:73
+#: src/cta/Payment/views.tsx:346
#, c-format
-msgid "Could not get details for withdraw operation:"
+msgid "Pay &nbsp; %1$s"
msgstr ""
-#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#: src/cta/Payment/views.tsx:360
#, c-format
-msgid "Chose different exchange provider"
+msgid "You have no balance for this currency. Withdraw digital cash first."
msgstr ""
-#: src/webex/pages/withdraw.tsx:109
+#: src/cta/Payment/views.tsx:364
#, c-format
msgid ""
-"Please select an exchange. You can review the details before after your "
-"selection."
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and "
+"is valid for a period of time. The exchange will charge the indicated amount "
+"every time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is "
+"valid for a period of time. The exchange will charge the indicated amount "
+"every time a transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will "
+"send the coins to this wallet after receiving a wire transfer with the "
+"correct subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr "Inizia a prelevare"
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid ""
+"To complete the process you need to wire%1$s %2$s to the exchange bank "
+"account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a "
+"banking app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr "ok"
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check "
+"the preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE "
+"ALL YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
msgstr ""
-#: src/webex/pages/withdraw.tsx:121
+#: src/wallet/DestinationSelection.tsx:449
#, c-format
-msgid "Select %1$s"
+msgid "currency not provided"
msgstr ""
-#: src/webex/pages/withdraw.tsx:143
+#: src/wallet/DestinationSelection.tsx:459
#, c-format
-msgid "Select custom exchange"
+msgid "Specify the amount and the destination"
msgstr ""
-#: src/webex/pages/withdraw.tsx:163
+#: src/wallet/DestinationSelection.tsx:483
#, c-format
-msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgid "Use previous destinations:"
msgstr ""
-#: src/webex/pages/withdraw.tsx:174
+#: src/wallet/DestinationSelection.tsx:503
#, c-format
-msgid "Accept fees and withdraw"
+msgid "Or specify the destination of the money"
msgstr ""
-#: src/webex/pages/withdraw.tsx:192
+#: src/wallet/DestinationSelection.tsx:511
#, c-format
-msgid "Cancel withdraw operation"
+msgid "Specify the destination of the money"
msgstr ""
-#: src/webex/renderHtml.tsx:249
+#: src/wallet/DestinationSelection.tsx:521
#, c-format
-msgid "Withdrawal fees:"
+msgid "To my bank account"
msgstr ""
-#: src/webex/renderHtml.tsx:252
+#: src/wallet/DestinationSelection.tsx:534
#, c-format
-msgid "Rounding loss:"
+msgid "To another wallet"
msgstr ""
-#: src/webex/renderHtml.tsx:254
+#: src/cta/Recovery/views.tsx:30
#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
+msgid "Could not load backup recovery information"
msgstr ""
-#: src/webex/renderHtml.tsx:262
+#: src/cta/Recovery/views.tsx:47
#, c-format
-msgid "# Coins"
+msgid "Digital wallet recovery"
msgstr ""
-#: src/webex/renderHtml.tsx:263
+#: src/cta/Recovery/views.tsx:52
#, c-format
-msgid "Value"
+msgid "Import backup, show info"
msgstr ""
-#: src/webex/renderHtml.tsx:264
+#: src/wallet/Application.tsx:189
#, c-format
-msgid "Withdraw Fee"
+msgid "All done, your transaction is in progress"
msgstr ""
-#: src/webex/renderHtml.tsx:265
+#: src/components/EditableText.tsx:45
#, c-format
-msgid "Refresh Fee"
+msgid "Edit"
msgstr ""
-#: src/webex/renderHtml.tsx:266
+#: src/wallet/ManualWithdrawPage.tsx:102
#, c-format
-msgid "Deposit Fee"
+msgid "Could not load the list of known exchanges"
msgstr ""
diff --git a/packages/taler-wallet-webextension/src/i18n/ja.po b/packages/taler-wallet-webextension/src/i18n/ja.po
new file mode 100644
index 000000000..298ad6018
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/ja.po
@@ -0,0 +1,1976 @@
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2023-03-06 22:06+0000\n"
+"Last-Translator: Anonymous <noreply@weblate.org>\n"
+"Language-Team: Japanese <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/ja/>\n"
+"Language: ja\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "残高"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "バックアップ"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr ""
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "設定"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr ""
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr ""
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms "
+"of service"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr ""
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr ""
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an "
+"minimum amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two "
+"additional recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid ""
+"Make sure the amount show %1$s BTC, else you have to change the base unit to "
+"BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to "
+"get the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and "
+"check there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid ""
+"Bank has confirmed the wire transfer. Waiting for the exchange to send the "
+"coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid ""
+"Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, fuzzy, c-format
+msgid "Withdraw"
+msgstr "撤退"
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, fuzzy, c-format
+msgid "Your current balance is not enough."
+msgstr "表示するバランスがありません"
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, fuzzy, c-format
+msgid "Withdrawals"
+msgstr "撤退"
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and "
+"is valid for a period of time. The exchange will charge the indicated amount "
+"every time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is "
+"valid for a period of time. The exchange will charge the indicated amount "
+"every time a transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will "
+"send the coins to this wallet after receiving a wire transfer with the "
+"correct subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid ""
+"To complete the process you need to wire%1$s %2$s to the exchange bank "
+"account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a "
+"banking app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check "
+"the preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE "
+"ALL YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
+
+#~ msgid ""
+#~ "To withdraw money you can start from your bank site or click the "
+#~ "\"withdraw\" button to use a known exchange."
+#~ msgstr ""
+#~ "お金を引き出すには、銀行のサイトから開始するか、[引き出し]ボタンをクリック"
+#~ "して既知の取引所を使用します。"
diff --git a/packages/taler-wallet-webextension/src/i18n/nl.po b/packages/taler-wallet-webextension/src/i18n/nl.po
new file mode 100644
index 000000000..4f11592dd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/nl.po
@@ -0,0 +1,1953 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-02 16:54+0000\n"
+"Last-Translator: Midgard <midgard@users.noreply.weblate.taler.net>\n"
+"Language-Team: Dutch <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/nl/>\n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr ""
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr ""
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr ""
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr ""
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr ""
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr ""
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr ""
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr "Beurs"
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
diff --git a/packages/taler-wallet-webextension/src/i18n/poheader b/packages/taler-wallet-webextension/src/i18n/poheader
index 3ec704932..a793df7ab 100644
--- a/packages/taler-wallet-webextension/src/i18n/poheader
+++ b/packages/taler-wallet-webextension/src/i18n/poheader
@@ -1,16 +1,16 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
#
-# TALER is free software; you can redistribute it and/or modify it under the
+# GNU Taler is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3, or (at your option) any later version.
#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
#, fuzzy
msgid ""
diff --git a/packages/taler-wallet-webextension/src/i18n/ru.po b/packages/taler-wallet-webextension/src/i18n/ru.po
new file mode 100644
index 000000000..aa002c984
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/ru.po
@@ -0,0 +1,1977 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-05-10 00:13+0000\n"
+"Last-Translator: Lily Ponomareva <lilyponomareva2017@gmail.com>\n"
+"Language-Team: Russian <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/ru/>\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.4.3\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Баланс"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "Резервная копия"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr "Считыватель QR-кодов и URI Taler"
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Настройки"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr "Dev"
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr "ОЖИДАЮЩИЕ ОПЕРАЦИИ"
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Загружаются"
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr "Не удалось загрузить поставщиков резервного копирования"
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr "Поставщики резервного копирования не настроены"
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr "Добавить сервис"
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr "Синхронизация всех резервных копий"
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr "Синхронизировать сейчас"
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr "Последняя синхронизация"
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr "Не синхронизировано"
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr "Срок действия истекает в"
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr ""
+"Произошла ошибка при загрузке сведений о поставщике для &quot; %1$s&quot;"
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr "Нет провайдера с url &quot;%1$s&quot;."
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr "Посмотреть провайдеров"
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr "Последняя резервная копия"
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr "Создать резервную копию"
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr "Комиссия провайдера"
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr "в год"
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr "Расширить"
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
+msgstr ""
+"изменились условия, продление сервиса будет означать принятие новых условий "
+"предоставления услуг"
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr "старый"
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr "новый"
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr "комиссия"
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr "хранение"
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr "Удалить провадер"
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr "Этот провайдер сообщил об ошибке"
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr "Возник конфликт с другой резервной копией из %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr "Резервная копия не читается"
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr "Неизвестная проблема резервного копирования: %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr "Услуга платная"
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr "Резервная копия действительна до"
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Отмена"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Открыть резервную страницу"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Открыть страницу оплаты"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Открыть страницу возврата средств"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "Открыть страницу чаевых"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Открыть страницу вывода средств"
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr "Получите цифровую наличность"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "Не удалось загрузить страницу баланса"
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr "Добавить"
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr "Отправить %1$s"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr "Действие Талер"
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr "На этой странице есть платное действие."
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr "На этой странице есть действие по выводу средств."
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr "На этой странице есть действие чаевых."
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr "На этой странице есть действие уведомить о резерве."
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr "Уведомить"
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr "На этой странице есть действие по возврату средств."
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr "На этой странице неправильно сформирован Taler URI."
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr "Закрыть"
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr "Это всплывающее окно закрывается и вы перенаправляетесь на %1$s"
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr "Не удалось загрузить сведения о предложении покупки"
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr "Номер заказа"
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr "Вкратце"
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr "Сумма"
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr "Название продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr "Юрисдикция продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr "Адрес продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr "Логотип продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr "Сайт продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr "Email продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr "Публичный ключ продавца"
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr "Дата поставки"
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr "Адрес доставки"
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr "Продукты"
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr "Создано в"
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr "Крайний срок возврата средств"
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr "Автоматический возврат средств"
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr "Крайний срок оплаты"
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr "URL-адрес выполнения"
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr "Сообщение о выполнении"
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr "Максимальная комиссия за депозит"
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr "максимальная комиссия"
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr "Минимальный возраст"
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr "Комиссия за банковский перевод"
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr "Аудиторы"
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr "Обменники"
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr "Баковский счёт"
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr "Биткоин адрес"
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr "IBAN"
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr "Не удалось загрузить статус депозита"
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr "Депозит цифровой налички"
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr "Стоимость"
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr "Комиссия"
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr "К получению"
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr "Отправить &nbsp; %1$s"
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr "Подробности перевода биткоина"
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+"Обменнику нужна транзакция с 3 выходами, один выход - это счёт обменника, а "
+"два других - это сегвит фейк адрес для метаданных с минимальной суммой."
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+"Убедитесь что сумма показывает %1$s BTC, в противном случае вам придётся "
+"изменить базовую единицу на BTC"
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr "Счёт"
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr "Хост банка"
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr "Подробности банковского перевода"
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr "Причина"
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr "Имя получателя"
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr "Не удалось загрузить информацию о транзакции"
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr "При попытке завершить транзакцию произошла ошибка"
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr "Эта транзакция не завершена"
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr "Отправить"
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr "Повторить попытку"
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr "Забыть"
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr "Внимание!"
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+"Если вы уже перевели деньги на обменник вы потеряете шанс получить монеты с "
+"нее."
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr "Подтвердить"
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr "Вывод"
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+"Убедитесь что вы указали правильное назначение, иначе деньги не поступят на "
+"этот кошелек."
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+"Банк пока не подтвердил перевод. Перейдите к %1$s %2$s и проверьте нет ли "
+"ожидающих шагов."
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr "Банк подтвердил перевод. Ожидание пока обменик отправит монеты"
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr "Подробности"
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr "Платёж"
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr "Возвраты"
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr "%1$s %2$s на %3$s"
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+"Продавец создал возврат средств за этот заказ, но не был автоматически "
+"забран."
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr "Предложение"
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr "Принять"
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr "Продавец"
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr "№ счёта-фактуры"
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr "Депозит"
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr "Обновить"
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr "Чаевые"
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr "Возврат"
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr "№ исходного заказа"
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr "Сводка о покупке"
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr "копировать"
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr "спрятать qr"
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr "показать qr"
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr "Кредит"
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr "Счёт-фактура"
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr "Обменник"
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr "URI"
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr "Дебит"
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr "Перевести"
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr "Страна"
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr "Строки адреса"
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr "Номер дома"
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr "Название дома"
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr "Улица"
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr "Почтовый индекс"
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr "Область города"
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr "Город"
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr "Район"
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr "Регион страны"
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr "Дата"
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr "Комиссия транзакции"
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr "Всего"
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr "Снять средства"
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr "Цена"
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr "Возвращено на счёт"
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr "Поставка"
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr "Итого перевод"
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr "Не удалось загрузить статус оплаты"
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr "Оплата цифровой наличкой"
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr "Покупка"
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr "Чек"
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr "Действительно до"
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr "Список продуктов"
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr "комиссия"
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr "Уже оплачено, вы будете перенаправлены на %1$s"
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr "Уже оплачено"
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr "Уже заявлено"
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr "Оплата с помощью мобильного телефона"
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr "Скрыть QR"
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr "Отсканируйте QR код или &nbsp; %1$s"
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr "Заплатить &nbsp; %1$s"
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr "У вас нет баланса в этой валюте. Сначала снимите цифровые деньги."
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+"Не удалось найти достаточно монет для оплаты. Даже если у вас достаточно %1$"
+"s, могут применяться некоторые ограничения."
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr "Недостаточно средств на балансе."
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr "Сообщение продавца"
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr "Не удалось загрузить статус возврата"
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr "Возврат цифровой налички"
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr "Вы проигнорировали чаевые."
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr "Возврат средств в выполняется."
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr "Всего к возврату"
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr "Продавец «%1$s»‎ предлагает вам возврат средств."
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr "Сумма заказа"
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr "Уже возвращено"
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr "Предложен возврат средств"
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr "Принять &nbsp; %1$s"
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr "Не удалось загрузить статус чаевых"
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr "Чаевые цифровой налички"
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr "Продавец предлагает вам чаевые"
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr "URL-адрес продавца"
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr "Получить &nbsp; %1$s"
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr "Чаевые от %1$s приняты. Проверьте список транзакций для подробностей."
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr "Выберете одну опцию"
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr "Невозможно загрузить"
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr "Показать Условия использования"
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr "Я принимаю эти Условия использования"
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr "Обменник не имеет условий использования"
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr "Ознакомиться с Условиями использования"
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr "Ознакомиться с новой версией Условий использования"
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr "Биржа ответитила с пустыми условиями использования"
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr "Скачать Условия использования"
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr "Скрыть Условия использования"
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr "Не удалось загрузить комиссию за обмен"
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr "Закрыть"
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr "Не удалось найти ни одного обменника"
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr "Не удалось найти ни одного обменника для валюты %1$s"
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr "Описание комиссии за услугу"
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr "Выберите %1$s обменник"
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr "Сбросить"
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr "Использовать этот обменник"
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr "Не имеет аудиторов"
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr "валюта"
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr "Операции"
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr "Депозиты"
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr "Деноминация"
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr "до"
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr "Выводы средств"
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr "Валюта"
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr "Операции моент"
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+"Каждая операция в этом разделе может отличаться номиналом и действительна в "
+"течение определенного периода времени. Биржа будет взимать указанную сумму "
+"каждый раз, когда монета используется в такой операции."
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr "Операции переводов"
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+"Каждая операция в этом разделе может отличаться в зависимости от типа "
+"перевода и действительна в течение определенного периода времени. Обменник "
+"будет взимать указанную сумму каждый раз при совершении перевода."
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr "Операция"
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr "Операции кошелька"
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr "Возможность"
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr "Не удалось получить информацию из URI"
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr "Не удалось получить информацию о выводе средств"
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr "Вывод цифровых наличных"
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr "Ограничения возраста"
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr "Вывести &nbsp; %1$s"
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr "Вывести на мобильный телефон"
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr "Цифровой счёт-фактура"
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr "Создать"
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr "Добавить Обменник"
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr "Начать вывод"
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr "Добавить Счёт"
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr "Выберете счёт"
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr "Добавить другой счёт"
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr "Комиссия депозита"
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr "Всего к депозиту"
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr "Выберете тип счёта"
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr "URL обменника"
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr "Добавить Обменник"
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr "Добавить новый Обменник"
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr "загрузка"
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr "Версия"
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr "Далее"
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr "Ожидание подтверждения"
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr "ОЖИДАЕТ"
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr "Добавить провайдера резервной копии"
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr "URL"
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr "Название"
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr "URL провайдера"
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr "комиссия за пополнение"
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr "Хранилище"
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr "Навигатор"
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr "Доверять"
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr "Условия использования"
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr "ok"
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr "изменено"
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr "не принято"
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr "Исправление проблем"
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr "Режим разработчика"
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr "Отбражение"
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr "Расширение браузера"
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr "Разрешения"
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr "Следующий шаг"
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr "Попробовать демо"
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr "Узнайте как пополнить ваш баланс на кошельке"
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr "Инструменты отладки"
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr "сбросить"
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr "импортировать базу данных"
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr "экспортировать базу данных"
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr "Монеты"
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr "значение"
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr "статус"
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr "кликите чтобы показать"
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Открыть"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr "Из моего банковского счёта"
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr "На мой банковский счёт"
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr "На другой кошелек"
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr "Импорт резервной копии, отображение информации"
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr "Все готово, ваша транзакция выполняется"
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr "Изменить"
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr "Не удалось загрузить список известных обменников"
diff --git a/packages/taler-wallet-webextension/src/i18n/strings-prelude b/packages/taler-wallet-webextension/src/i18n/strings-prelude
index aa6602bd4..7d9d13136 100644
--- a/packages/taler-wallet-webextension/src/i18n/strings-prelude
+++ b/packages/taler-wallet-webextension/src/i18n/strings-prelude
@@ -1,17 +1,17 @@
/*
- This file is part of TALER
- (C) 2016 Inria
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
export const strings: {[s: string]: any} = {};
diff --git a/packages/taler-wallet-webextension/src/i18n/strings.ts b/packages/taler-wallet-webextension/src/i18n/strings.ts
index 5b1257830..e5281bf54 100644
--- a/packages/taler-wallet-webextension/src/i18n/strings.ts
+++ b/packages/taler-wallet-webextension/src/i18n/strings.ts
@@ -1,443 +1,3191 @@
-/*
- This file is part of TALER
- (C) 2016 Inria
+export const strings: any = {};
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-export const strings: { [s: string]: any } = {};
strings["de"] = {
domain: "messages",
locale_data: {
messages: {
"": {
domain: "messages",
- plural_forms: "nplurals=2; plural=(n != 1);",
- lang: "",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
},
- "Invalid Wire": [""],
- "Invalid Test Wire Detail": [""],
- "Test Wire Acct #%1$s on %2$s": [""],
- "Unknown Wire Detail": [""],
- Operation: [""],
- "time (ms/op)": [""],
- "The merchant %1$s offers you to purchase:": [
- "Der Händler %1$s möchte einen Vertrag über %2$s mit Ihnen abschließen.",
- ],
- "The total price is %1$s (plus %2$s fees).": [""],
- "The total price is %1$s.": [""],
- Retry: [""],
- "Confirm payment": ["Bezahlung bestätigen"],
- Balance: ["Saldo"],
- History: ["Verlauf"],
- Debug: ["Debug"],
- "You have no balance to show. Need some %1$s getting started?": [
- "Sie haben kein Digitalgeld. Wollen Sie %1$s? abheben?",
- ],
- "%1$s incoming": [""],
- "%1$s being spent": [""],
- "Error: could not retrieve balance information.": [""],
- "Invalid ": [""],
- "Fees ": [""],
- "Refresh sessions has completed": [""],
- "Order Refused": [""],
- "Order redirected": [""],
- "Payment aborted": [""],
- "Payment Sent": [""],
- "Order accepted": [""],
- "Reserve balance updated": [""],
- "Payment refund": [""],
- Withdrawn: ["Abheben bei %1$s"],
- "Tip Accepted": [""],
- "Tip Declined": [""],
+ Balance: ["Guthaben"],
+ Backup: ["Backup"],
+ "QR Reader and Taler URI": [""],
+ Settings: ["Einstellungen"],
+ Dev: ["Dev"],
"%1$s": [""],
- "Your wallet has no events recorded.": [
- "Ihre Geldbörse verzeichnet keine Vorkommnisse.",
- ],
- "Wire to bank account": [""],
- Confirm: ["Bezahlung bestätigen"],
- Cancel: ["Saldo"],
- "Could not get details for withdraw operation:": [""],
- "Chose different exchange provider": [""],
- "Please select an exchange. You can review the details before after your selection.": [
+ "PENDING OPERATIONS": [""],
+ Loading: ["Lädt Daten"],
+ "Could not load backup providers": [""],
+ "No backup providers configured": [""],
+ "Add provider": [""],
+ "Sync all backups": [""],
+ "Sync now": [""],
+ "Last synced": [""],
+ "Not synced": [""],
+ "Expires in": [""],
+ "There was an error loading the provider detail for &quot; %1$s&quot;": [
"",
],
- "Select %1$s": [""],
- "Select custom exchange": [""],
- "You are about to withdraw %1$s from your bank account into your wallet.": [
+ "There is not known provider with url &quot;%1$s&quot;.": [""],
+ "See providers": [""],
+ "Last backup": [""],
+ "Back up": [""],
+ "Provider fee": [""],
+ "per year": [""],
+ Extend: [""],
+ "terms has changed, extending the service will imply accepting the new terms of service":
+ [""],
+ old: [""],
+ new: [""],
+ fee: [""],
+ storage: [""],
+ "Remove provider": [""],
+ "This provider has reported an error": [""],
+ "There is conflict with another backup from %1$s": [""],
+ "Backup is not readable": [""],
+ "Unknown backup problem: %1$s": [""],
+ "service paid": [""],
+ "Backup valid until": [""],
+ Cancel: ["Abbrechen"],
+ "Open reserve page": ["Seite der Reserve aufrufen"],
+ "Open pay page": ["Seite für Zahlungen aufrufen"],
+ "Open refund page": ["Seite für Rückerstattungen aufrufen"],
+ "Open tip page": ["Seite der Aufwandsentschädigungen aufrufen"],
+ "Open withdraw page": ["Abhebeseite öffnen"],
+ "Get digital cash": ["Digitales Bargeld abheben"],
+ "Could not load balance page": ["Konnte die Umsatzanzeige nicht laden"],
+ Add: [""],
+ "Send %1$s": [""],
+ "Taler Action": [""],
+ "This page has pay action.": [""],
+ "This page has a withdrawal action.": [""],
+ "This page has a tip action.": [""],
+ "This page has a notify reserve action.": [""],
+ Notify: [""],
+ "This page has a refund action.": [""],
+ "This page has a malformed taler uri.": [""],
+ Dismiss: [""],
+ "this popup is being closed and you are being redirected to %1$s": [""],
+ "Could not load purchase proposal details": [
+ "Konnte die Umsatzanzeige nicht laden",
+ ],
+ "Order Id": [""],
+ Summary: [""],
+ Amount: ["Betrag"],
+ "Merchant name": [""],
+ "Merchant jurisdiction": [""],
+ "Merchant address": [""],
+ "Merchant logo": [""],
+ "Merchant website": [""],
+ "Merchant email": [""],
+ "Merchant public key": [""],
+ "Delivery date": [""],
+ "Delivery location": [""],
+ Products: [""],
+ "Created at": [""],
+ "Refund deadline": [""],
+ "Auto refund": [""],
+ "Pay deadline": [""],
+ "Fulfillment URL": [""],
+ "Fulfillment message": [""],
+ "Max deposit fee": [""],
+ "Max fee": [""],
+ "Minimum age": [""],
+ "Wire fee amortization": [""],
+ Auditors: [""],
+ Exchanges: ["Exchange"],
+ "Bank account": [""],
+ "Bitcoin address": [""],
+ IBAN: [""],
+ "Could not load deposit status": ["Konnte die Umsatzanzeige nicht laden"],
+ "Digital cash deposit": [""],
+ Cost: [""],
+ Fee: [""],
+ "To be received": [""],
+ "Send &nbsp; %1$s": [""],
+ "Bitcoin transfer details": [""],
+ "The exchange need a transaction with 3 output, one output is the exchange account and the other two are segwit fake address for metadata with an minimum amount.":
+ [""],
+ "In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional recipient and copy addresses and amounts":
+ [""],
+ "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC":
+ [""],
+ Account: [""],
+ "Bank host": [""],
+ "Bank transfer details": [""],
+ Subject: ["Verwendungszweck"],
+ "Receiver name": [""],
+ "Could not load the transaction information": [""],
+ "There was an error trying to complete the transaction": [""],
+ "This transaction is not completed": [""],
+ Send: [""],
+ Retry: ["Erneut versuchen"],
+ Forget: [""],
+ "Caution!": [""],
+ "If you have already wired money to the exchange you will loose the chance to get the coins form it.":
+ [""],
+ Confirm: ["Bestätigen"],
+ Withdrawal: ["Abheben"],
+ "Make sure to use the correct subject, otherwise the money will not arrive in this wallet.":
+ [""],
+ "The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check there is no pending step.":
+ [""],
+ "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins":
+ [""],
+ Details: [""],
+ Payment: ["Zahlung"],
+ Refunds: [""],
+ "%1$s %2$s on %3$s": [
+ "%1$s\n möchte einen Vertrag über %2$s\n mit Ihnen abschließen.",
+ ],
+ "Merchant created a refund for this order but was not automatically picked up.":
+ [""],
+ Offer: [""],
+ Accept: [""],
+ Merchant: [""],
+ "Invoice ID": [""],
+ Deposit: [""],
+ Refresh: [""],
+ Tip: [""],
+ Refund: [""],
+ "Original order ID": [""],
+ "Purchase summary": [""],
+ copy: [""],
+ "hide qr": [""],
+ "show qr": [""],
+ Credit: [""],
+ Invoice: [""],
+ Exchange: ["Exchange"],
+ URI: [""],
+ Debit: [""],
+ Transfer: [""],
+ Country: ["Betrag"],
+ "Address lines": [""],
+ "Building number": [""],
+ "Building name": [""],
+ Street: [""],
+ "Post code": [""],
+ "Town location": [""],
+ Town: [""],
+ District: [""],
+ "Country subdivision": [""],
+ Date: [""],
+ "Transaction fees": [""],
+ Total: [""],
+ Withdraw: ["Abheben"],
+ Price: [""],
+ Refunded: [""],
+ Delivery: [""],
+ "Total transfer": ["Insgesamt abgehoben"],
+ "Could not load pay status": [""],
+ "Digital cash payment": [""],
+ Purchase: [""],
+ Receipt: [""],
+ "Valid until": [""],
+ "List of products": [""],
+ free: [""],
+ "Already paid, you are going to be redirected to %1$s": [""],
+ "Already paid": [""],
+ "Already claimed": [""],
+ "Pay with a mobile phone": [""],
+ "Hide QR": [""],
+ "Scan the QR code or &nbsp; %1$s": [""],
+ "Pay &nbsp; %1$s": [""],
+ "You have no balance for this currency. Withdraw digital cash first.": [
"",
],
- "Accept fees and withdraw": [""],
- "Cancel withdraw operation": [""],
- "Withdrawal fees:": ["Abheben bei"],
- "Rounding loss:": [""],
- "Earliest expiration (for deposit): %1$s": [""],
- "# Coins": [""],
- Value: [""],
- "Withdraw Fee": ["Abheben bei %1$s"],
- "Refresh Fee": [""],
- "Deposit Fee": [""],
+ "Could not find enough coins to pay. Even if you have enough %1$s some restriction may apply.":
+ [""],
+ "Your current balance is not enough.": [
+ "Es gibt kein Guthaben anzuzeigen.",
+ ],
+ "Merchant message": [""],
+ "Could not load refund status": ["Konnte die Umsatzanzeige nicht laden"],
+ "Digital cash refund": [""],
+ "You&apos;ve ignored the tip.": [""],
+ "The refund is in progress.": [""],
+ "Total to refund": ["Insgesamt abgehoben"],
+ "The merchant &quot;%1$s&quot; is offering you a refund.": [
+ "Der Händler %1$s bietet Ihnen eine Aufwandsentschädigung von %2$s durch den Exchange %3$s",
+ ],
+ "Order amount": [""],
+ "Already refunded": [""],
+ "Refund offered": [""],
+ "Accept &nbsp; %1$s": [""],
+ "Could not load tip status": ["Konnte die Umsatzanzeige nicht laden"],
+ "Digital cash tip": [""],
+ "The merchant is offering you a tip": [
+ "Der Händler %1$s bietet Ihnen eine Aufwandsentschädigung von %2$s durch den Exchange %3$s",
+ ],
+ "Merchant URL": [""],
+ "Receive &nbsp; %1$s": [""],
+ "Tip from %1$s accepted. Check your transactions list for more details.":
+ [""],
+ "Select one option": [""],
+ "Could not load": ["Konnte die Umsatzanzeige nicht laden"],
+ "Show terms of service": [""],
+ "I accept the exchange terms of service": [""],
+ "Exchange doesn&apos;t have terms of service": [""],
+ "Review exchange terms of service": [""],
+ "Review new version of terms of service": [""],
+ "The exchange reply with a empty terms of service": [""],
+ "Download Terms of Service": [""],
+ "Hide terms of service": [""],
+ "Could not load exchange fees": ["Konnte die Umsatzanzeige nicht laden"],
+ Close: [""],
+ "could not find any exchange": ["Konnte die Umsatzanzeige nicht laden"],
+ "could not find any exchange for the currency %1$s": [""],
+ "Service fee description": [""],
+ "Select %1$s exchange": [""],
+ Reset: [""],
+ "Use this exchange": [""],
+ "Doesn&apos;t have auditors": [""],
+ currency: [""],
+ Operations: [""],
+ Deposits: ["%1$s zahlen"],
+ Denomination: [""],
+ Until: [""],
+ Withdrawals: ["Abheben"],
+ Currency: [""],
+ "Coin operations": [""],
+ "Every operation in this section may be different by denomination value and is valid for a period of time. The exchange will charge the indicated amount every time a coin is used in such operation.":
+ [""],
+ "Transfer operations": [""],
+ "Every operation in this section may be different by transfer type and is valid for a period of time. The exchange will charge the indicated amount every time a transfer is made.":
+ [""],
+ Operation: [""],
+ "Wallet operations": [""],
+ Feature: [""],
+ "Could not get the info from the URI": [""],
+ "Could not get info of withdrawal": [""],
+ "Digital cash withdrawal": [""],
+ "Could not finish the withdrawal operation": [""],
+ "Age restriction": [""],
+ "Withdraw &nbsp; %1$s": ["Abheben bei %1$s"],
+ "Withdraw to a mobile phone": [""],
+ "Digital invoice": [""],
+ "Could not finish the invoice creation": [""],
+ Create: [""],
+ "Could not finish the payment operation": [""],
+ "Digital cash transfer": [""],
+ "Could not finish the transfer creation": [""],
+ "Could not finish the pickup operation": [""],
+ "Manual Withdrawal for %1$s": ["Manuelles Abheben"],
+ "Choose a exchange from where the coins will be withdrawn. The exchange will send the coins to this wallet after receiving a wire transfer with the correct subject.":
+ [""],
+ "No exchange found for %1$s": [""],
+ "Add Exchange": [""],
+ "No exchange configured": [""],
+ "Can&apos;t create the reserve": [""],
+ "Start withdrawal": ["Abhebung beginnen"],
+ "Could not load deposit balance": [
+ "Konnte die Umsatzanzeige nicht laden",
+ ],
+ "A currency or an amount should be indicated": [""],
+ "There is no enough balance to make a deposit for currency %1$s": [""],
+ "Send %1$s to your account": [""],
+ "There is no account to make a deposit for currency %1$s": [""],
+ "Add account": [""],
+ "Select account": [""],
+ "Add another account": [""],
+ "Deposit fee": [""],
+ "Total deposit": [""],
+ "Deposit&nbsp;%1$s %2$s": ["Einlösen %1$s %2$s"],
+ "Add bank account for %1$s": [""],
+ "Enter the URL of an exchange you trust.": [""],
+ "Unable add this account": [""],
+ "Select account type": [""],
+ "Review terms of service": [""],
+ "Exchange URL": [""],
+ "Add exchange": [""],
+ "Add new exchange": [""],
+ "Add exchange for %1$s": [""],
+ "An exchange has been found! Review the information and click next": [""],
+ "This exchange doesn&apos;t match the expected currency %1$s": [""],
+ "Unable to verify this exchange": [""],
+ "Unable to add this exchange": [""],
+ loading: [""],
+ Version: [""],
+ Next: [""],
+ "Waiting for confirmation": [""],
+ PENDING: [""],
+ "Could not load the list of transactions": [""],
+ "Your transaction history is empty for this currency.": [""],
+ "Add backup provider": [""],
+ "Could not get provider information": [""],
+ "Backup providers may charge for their service": [""],
+ URL: [""],
+ Name: [""],
+ "Provider URL": [""],
+ "Please review and accept this provider&apos;s terms of service": [""],
+ Pricing: [""],
+ "free of charge": [""],
+ "%1$s per year of service": [""],
+ Storage: [""],
+ "%1$s megabytes of storage per year of service": [""],
+ "Accept terms of service": [""],
+ "Could not parse the payto URI": [""],
+ "Please check the uri": [""],
+ "Exchange is ready for withdrawal": [""],
+ "To complete the process you need to wire%1$s %2$s to the exchange bank account":
+ [""],
+ "Alternative, you can also scan this QR code or open %1$s if you have a banking app installed that supports RFC 8905":
+ [""],
+ "Cancel withdrawal": [""],
+ "Could not toggle auto-open": ["Konnte die Umsatzanzeige nicht laden"],
+ "Could not toggle clipboard": ["Konnte die Umsatzanzeige nicht laden"],
+ Navigator: [""],
+ "Automatically open wallet based on page content": [""],
+ "Enabling this option below will make using the wallet faster, but requires more permissions from your browser.":
+ [""],
+ "Automatically check clipboard for Taler URI": [""],
+ Trust: [""],
+ "No exchange yet": [""],
+ "Term of Service": [""],
+ ok: [""],
+ changed: [""],
+ "not accepted": [""],
+ "unknown (exchange status should be updated)": [""],
+ "Add an exchange": [""],
+ Troubleshooting: [""],
+ "Developer mode": [""],
+ "More options and information useful for debugging": [""],
+ Display: [""],
+ "Current Language": [""],
+ "Wallet Core": [""],
+ "Web Extension": [""],
+ "Exchange compatibility": [""],
+ "Merchant compatibility": [""],
+ "Bank compatibility": [""],
+ "Browser Extension Installed!": [""],
+ "You can open the GNU Taler Wallet using the combination %1$s .": [""],
+ "Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick access without keyboard:":
+ [""],
+ "Click the puzzle icon": [""],
+ "Search for GNU Taler Wallet": [""],
+ "Click the pin icon": [""],
+ Permissions: [""],
+ "(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)":
+ [""],
+ "Next Steps": [""],
+ "Try the demo": [""],
+ "Learn how to top up your wallet balance": [""],
+ "Diagnostics timed out. Could not talk to the wallet backend.": [
+ "Die Diagnostik ist abgeschlossen. Es war keine Kommunikation mit dem Wallet-Backend möglich.",
+ ],
+ "Problems detected:": ["Ein Problem wurde festgestellt:"],
+ "Please check in your %1$s settings that you have IndexedDB enabled (check the preference name %2$s).":
+ [
+ "Bitte prüfen Sie ihre %1$s Einstellungen, für die Sie IndexedDB verwenden (preference name %2$s prüfen).",
+ ],
+ "Your wallet database is outdated. Currently automatic migration is not supported. Please go %1$s to reset the wallet database.":
+ [
+ "Die Datenbank des Wallets ist veraltet. Aktuell wird jedoch keine Migration auf eine neue Version unterstützt. Bitte wählen Sie %1$s zum Zurücksetzen der Wallet-Datenbank.",
+ ],
+ "Running diagnostics": ["Diagnostik wird durchgeführt"],
+ "Debug tools": ["Debugging-Tools"],
+ "Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?":
+ [""],
+ reset: ["zurücksetzen"],
+ "TESTING: This may delete all your coin, proceed with caution": [""],
+ "run gc": [""],
+ "import database": [""],
+ "export database": [""],
+ "Database exported at %1$s %2$s to download": [""],
+ Coins: [""],
+ "Pending operations": [""],
+ "usable coins": [""],
+ id: [""],
+ denom: [""],
+ value: [""],
+ status: [""],
+ "from refresh?": [""],
+ "age key count": [""],
+ "spent coins": [""],
+ "click to show": [""],
+ "Scan a QR code or enter taler:// URI below": [""],
+ Open: [""],
+ "URI is not valid. Taler URI should start with `taler://`": [""],
+ "Try another": [""],
+ "Could not load list of exchange": [
+ "Konnte die Umsatzanzeige nicht laden",
+ ],
+ "Choose a currency to proceed or add another exchange": [""],
+ "Known currencies": [""],
+ "Specify the amount and the origin": [""],
+ "Change currency": [""],
+ "Use previous origins:": [""],
+ "Or specify the origin of the money": [""],
+ "Specify the origin of the money": [""],
+ "From my bank account": [""],
+ "From another wallet": [""],
+ "currency not provided": [""],
+ "Specify the amount and the destination": [""],
+ "Use previous destinations:": [""],
+ "Or specify the destination of the money": [""],
+ "Specify the destination of the money": [""],
+ "To my bank account": [""],
+ "To another wallet": [""],
+ "Could not load backup recovery information": [
+ "Konnte die Umsatzanzeige nicht laden",
+ ],
+ "Digital wallet recovery": [""],
+ "Import backup, show info": [""],
+ "All done, your transaction is in progress": [""],
+ Edit: [""],
+ "Could not load the list of known exchanges": [""],
},
},
};
-strings["en-US"] = {
+strings["es"] = {
domain: "messages",
locale_data: {
messages: {
"": {
domain: "messages",
- plural_forms: "nplurals=2; plural=(n != 1);",
- lang: "",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ },
+ Balance: ["Balance"],
+ Backup: ["Copia de seguridad"],
+ "QR Reader and Taler URI": ["Lector QR y Taler URI"],
+ Settings: ["Configuración"],
+ Dev: ["Dev"],
+ "%1$s": ["%1$s"],
+ "PENDING OPERATIONS": ["OPERACIONES PENDIENTES"],
+ Loading: ["Cargando"],
+ "Could not load backup providers": [
+ "No se pudo cargar los proveedores de copias de seguridad",
+ ],
+ "No backup providers configured": [
+ "No hay proveedores de copias de seguridad configurados",
+ ],
+ "Add provider": ["Agregar proveedor"],
+ "Sync all backups": ["Sincronizar todas las copias de seguridad"],
+ "Sync now": ["Sincronizar ahora"],
+ "Last synced": ["Ultima vez sincronizado"],
+ "Not synced": ["No sincronizado"],
+ "Expires in": ["Expira en"],
+ "There was an error loading the provider detail for &quot; %1$s&quot;": [
+ 'Hubo un error cargando los detalles del proveedor para "%1$s"',
+ ],
+ "There is not known provider with url &quot;%1$s&quot;.": [
+ 'No hay proveedor conocido con la URL "%1$s".',
+ ],
+ "See providers": ["Ver proveedores"],
+ "Last backup": ["Última copia de seguridad"],
+ "Back up": ["Copia de seguridad"],
+ "Provider fee": ["Tarifa del proveedor"],
+ "per year": ["por año"],
+ Extend: ["Extender"],
+ "terms has changed, extending the service will imply accepting the new terms of service":
+ [
+ "los términos han cambiado, extender el servicio implicará aceptar los nuevos términos de servicio",
+ ],
+ old: ["viejo"],
+ new: ["nuevo"],
+ fee: ["tarifa"],
+ storage: ["almacenamiento"],
+ "Remove provider": ["Eliminar proveedor"],
+ "This provider has reported an error": [
+ "Este proveedor ha reportado un error",
+ ],
+ "There is conflict with another backup from %1$s": [
+ "Hay un conflicto con otra copia de seguridad de %1$s",
+ ],
+ "Backup is not readable": ["La copia de seguridad no es legible"],
+ "Unknown backup problem: %1$s": [
+ "Problema de copia de seguridad desconocido: %1$s",
+ ],
+ "service paid": ["servicio pagado"],
+ "Backup valid until": ["Copia de seguridad válida hasta"],
+ Cancel: ["Cancelar"],
+ "Open reserve page": ["Abrir página de reserva"],
+ "Open pay page": ["Abrir página de pago"],
+ "Open refund page": ["Abrir página de devolución"],
+ "Open tip page": ["Abrir página de propina"],
+ "Open withdraw page": ["Abrir página de retirada"],
+ "Get digital cash": ["Retirar dinero digital"],
+ "Could not load balance page": ["No se pudo cargar la página"],
+ Add: ["Agregar"],
+ "Send %1$s": ["Envíar %1$s"],
+ "Taler Action": ["Acción Taler"],
+ "This page has pay action.": ["Esta página tiene una acción de pago."],
+ "This page has a withdrawal action.": [
+ "Esta página tiene una acción de retirada.",
+ ],
+ "This page has a tip action.": [
+ "Esta página tiene una acción de propina.",
+ ],
+ "This page has a notify reserve action.": [
+ "Esta página tiene una acción de notificación de reserva.",
+ ],
+ Notify: ["Notificar"],
+ "This page has a refund action.": [
+ "Esta página tiene una acción de devolución.",
+ ],
+ "This page has a malformed taler uri.": [
+ "Esta página tiene una URI de Taler malformada.",
+ ],
+ Dismiss: ["Descartar"],
+ "this popup is being closed and you are being redirected to %1$s": [
+ "Este popup está siendo cerrado y estás siendo redirigido a %1$s",
+ ],
+ "Could not load purchase proposal details": [
+ "No se pudo cargar el detalle de la propuesta",
+ ],
+ "Order Id": ["Id de orden"],
+ Summary: ["Resumen"],
+ Amount: ["Cantidad"],
+ "Merchant name": ["Comerciante"],
+ "Merchant jurisdiction": ["Jurisdicción"],
+ "Merchant address": ["Dirección del comerciante"],
+ "Merchant logo": ["Logo"],
+ "Merchant website": ["Siti web"],
+ "Merchant email": ["Correo electrónico"],
+ "Merchant public key": ["Clave pública"],
+ "Delivery date": ["Fecha de entrega"],
+ "Delivery location": ["Ubicación de entrega"],
+ Products: ["Productos"],
+ "Created at": ["Creado en"],
+ "Refund deadline": ["Plazo de devolución"],
+ "Auto refund": ["Devolución automática"],
+ "Pay deadline": ["Plazo de pago"],
+ "Fulfillment URL": ["URL de éxito"],
+ "Fulfillment message": ["Mensaje de éxito"],
+ "Max deposit fee": ["Máxima comisión de depósito"],
+ "Max fee": ["Máxima comisión"],
+ "Minimum age": ["Edad mínima"],
+ "Wire fee amortization": ["Amortización de comisión de transferencia"],
+ Auditors: ["Auditores"],
+ Exchanges: ["Exchanges"],
+ "Bank account": ["Cuenta del banco"],
+ "Bitcoin address": ["Dirección de Bitcoin"],
+ IBAN: ["IBAN"],
+ "Could not load deposit status": [
+ "No se pudo cargar el estado del depósito",
+ ],
+ "Digital cash deposit": ["Depósito de dinero digital"],
+ Cost: ["Costo"],
+ Fee: ["Comisión"],
+ "To be received": ["A recibir"],
+ "Send &nbsp; %1$s": ["Envíar %1$s"],
+ "Bitcoin transfer details": ["Detalle de transferencia Bitcoin"],
+ "The exchange need a transaction with 3 output, one output is the exchange account and the other two are segwit fake address for metadata with an minimum amount.":
+ [
+ "El exchange necesita una transacción con 3 salidas, una salida es hacia la cuenta del exchange y las otras dos son direcciones segwit falsas para metadata con el monto mínimo.",
+ ],
+ "In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional recipient and copy addresses and amounts":
+ [
+ 'En la billetera bitcoincore usar el botón "Agregar destinatario" para agregar dos destinatarios y copiar las direcciones y montos',
+ ],
+ "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC":
+ [
+ "Asegurarse de que el monto muestre %1$s BTC, sino tendrá que cambiar la unidad a BTC",
+ ],
+ Account: ["Cuenta"],
+ "Bank host": ["Banco anfitrión"],
+ "Bank transfer details": ["Detalle de transferencia bancaria"],
+ Subject: ["Asunto"],
+ "Receiver name": ["Nombre del receptor"],
+ "Could not load the transaction information": [
+ "No se pudo cargar información de la transacción",
+ ],
+ "There was an error trying to complete the transaction": [
+ "Hubo un error intentando completar la transacción",
+ ],
+ "This transaction is not completed": [
+ "Esta transacción no está completada",
+ ],
+ Send: ["Enviar"],
+ Retry: ["Reintentar"],
+ Forget: ["Olvidar"],
+ "Caution!": ["Cuidado!"],
+ "If you have already wired money to the exchange you will loose the chance to get the coins form it.":
+ [
+ "Si tú ya has transferido dinero al exchange, perderás la oportunidad de recibir las monedas desde este.",
+ ],
+ Confirm: ["Confirmar"],
+ Withdrawal: ["Retirada"],
+ "Make sure to use the correct subject, otherwise the money will not arrive in this wallet.":
+ [
+ "Asegúrate de usar el asunto correcto, de lo contrario el dinero no llegará a esta billetera.",
+ ],
+ "The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check there is no pending step.":
+ [
+ "El banco todavía no confirmó la transferencia. Ir a %1$s %2$s y verificar que no hay pasos pendientes.",
+ ],
+ "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins":
+ [
+ "El banco confirmó la transferencia. Esperando que el exchange envíe las monedas",
+ ],
+ Details: ["Detalles"],
+ Payment: ["Pago"],
+ Refunds: ["Devoluciones"],
+ "%1$s %2$s on %3$s": ["%1$s %2$s en %3$s"],
+ "Merchant created a refund for this order but was not automatically picked up.":
+ [
+ "El comerciante creó una devolución para esta orden pero no fue recogida automáticamente.",
+ ],
+ Offer: ["Oferta"],
+ Accept: ["Aceptar"],
+ Merchant: ["Comerciante"],
+ "Invoice ID": ["Id de factura"],
+ Deposit: ["Depósito"],
+ Refresh: ["Actualizar"],
+ Tip: ["Propina"],
+ Refund: ["Devolución"],
+ "Original order ID": ["Id de orden original"],
+ "Purchase summary": ["Resumen de compra"],
+ copy: ["Copiar"],
+ "hide qr": ["Esconder QR"],
+ "show qr": ["Mostrar QR"],
+ Credit: ["Crédito"],
+ Invoice: ["Factura"],
+ Exchange: ["Exchange"],
+ URI: ["URI"],
+ Debit: ["Débito"],
+ Transfer: ["Transferencia"],
+ Country: ["País"],
+ "Address lines": ["Detalle de dirección"],
+ "Building number": ["Número de edificio"],
+ "Building name": ["Nombre de edificio"],
+ Street: ["Calle"],
+ "Post code": ["Código postal"],
+ "Town location": ["Ubicación de la ciudad"],
+ Town: ["Ciudad"],
+ District: ["Distrito"],
+ "Country subdivision": ["Subdivisión de país"],
+ Date: ["Fecha"],
+ "Transaction fees": ["Comisiones de transacción"],
+ Total: ["Total"],
+ Withdraw: ["Retirar"],
+ Price: ["Precio"],
+ Refunded: ["Devuelto"],
+ Delivery: ["Entrega"],
+ "Total transfer": ["Total transferido"],
+ "Could not load pay status": ["No se pudo cargar el estado del pago"],
+ "Digital cash payment": ["Pago con dinero digital"],
+ Purchase: ["Compra"],
+ Receipt: ["Recibo"],
+ "Valid until": ["Válido hasta"],
+ "List of products": ["Lista de productos"],
+ free: ["Gratis"],
+ "Already paid, you are going to be redirected to %1$s": [
+ "Ya pagado, estás siendo dirigido a %1$s",
+ ],
+ "Already paid": ["Ya pagado"],
+ "Already claimed": ["Ya reclamado"],
+ "Pay with a mobile phone": ["Pagar con un teléfono móbil"],
+ "Hide QR": ["Esconder QR"],
+ "Scan the QR code or &nbsp; %1$s": ["Escanear el código QR o %1$s"],
+ "Pay &nbsp; %1$s": ["Pagar %1$s"],
+ "You have no balance for this currency. Withdraw digital cash first.": [
+ "No hay balance para esta divisa. Extraer dinero digital primero.",
+ ],
+ "Could not find enough coins to pay. Even if you have enough %1$s some restriction may apply.":
+ [
+ "No se encontraron suficientes monedas para pagar. Incluso si tuviera suficiente %1$s algunas restricciones se podrían aplicar.",
+ ],
+ "Your current balance is not enough.": ["Tu balance no es suficiente."],
+ "Merchant message": ["Mensaje del comerciante"],
+ "Could not load refund status": [
+ "No se pudo cargar el estado de la devolución",
+ ],
+ "Digital cash refund": ["Devolución de dinero digital"],
+ "You&apos;ve ignored the tip.": ["Has ignorado la propina."],
+ "The refund is in progress.": [
+ "El proceso de devolución está en progreso.",
+ ],
+ "Total to refund": ["Total para devolver"],
+ "The merchant &quot;%1$s&quot; is offering you a refund.": [
+ 'El comerciante "%1$s" te está ofreciendo una devolución.',
+ ],
+ "Order amount": ["Monto de la orden"],
+ "Already refunded": ["Ya devuelto"],
+ "Refund offered": ["Devolución ofrecida"],
+ "Accept &nbsp; %1$s": ["Aceptar %1$s"],
+ "Could not load tip status": [
+ "No se pudo cargar el estado de la propina",
+ ],
+ "Digital cash tip": ["Propina con dinero digital"],
+ "The merchant is offering you a tip": [
+ "El comerciante te ofrece una propina",
+ ],
+ "Merchant URL": ["URL del comerciante"],
+ "Receive &nbsp; %1$s": ["Recibir %1$s"],
+ "Tip from %1$s accepted. Check your transactions list for more details.":
+ [
+ "Propina de %1$s aceptada. Revisa tu lista de transacciones para más detalle.",
+ ],
+ "Select one option": ["Seleccione una opción"],
+ "Could not load": ["No se pudo cargar"],
+ "Show terms of service": ["Mostrar términos de servicio"],
+ "I accept the exchange terms of service": [
+ "Yo acepto los términos de servicio del exchange",
+ ],
+ "Exchange doesn&apos;t have terms of service": [
+ "El exchange no tiene los términos de servicio",
+ ],
+ "Review exchange terms of service": ["Revisar los términos de servicio"],
+ "Review new version of terms of service": [
+ "Revisar los nuevos términos de servicio",
+ ],
+ "The exchange reply with a empty terms of service": [
+ "El exchange respondió con unos términos de servicio vacíos",
+ ],
+ "Download Terms of Service": ["Descargar los términos de servicio"],
+ "Hide terms of service": ["Esconder los términos de servicio"],
+ "Could not load exchange fees": [
+ "No se pudo cargar la comisión del exchange",
+ ],
+ Close: ["Cerrar"],
+ "could not find any exchange": ["No se pudo encontrar ningún exchange"],
+ "could not find any exchange for the currency %1$s": [
+ "No se pudo encontrar ningún exchange para la divisa %1$s",
+ ],
+ "Service fee description": ["Descripción de comisión de servicio"],
+ "Select %1$s exchange": ["Seleccionar exchange %1$s"],
+ Reset: ["Reiniciar"],
+ "Use this exchange": ["Usar este exchange"],
+ "Doesn&apos;t have auditors": ["No tiene auditores"],
+ currency: ["Divisa"],
+ Operations: ["Operaciones"],
+ Deposits: ["Depósitos"],
+ Denomination: ["Operaciones"],
+ Until: ["Hasta"],
+ Withdrawals: ["Retiradas"],
+ Currency: ["Divisa"],
+ "Coin operations": ["Operaciones de moneda"],
+ "Every operation in this section may be different by denomination value and is valid for a period of time. The exchange will charge the indicated amount every time a coin is used in such operation.":
+ [
+ "Toda operación en esta sección puede ser diferente por valor de denominación y es válida por un período. El exchange cobrará el monto indicado cada vez que una es usada en dicha operación.",
+ ],
+ "Transfer operations": ["Operaciones de transferencia"],
+ "Every operation in this section may be different by transfer type and is valid for a period of time. The exchange will charge the indicated amount every time a transfer is made.":
+ [
+ "Toda operación en esta sección puede ser diferente por tipo de transacción y es válida por un período. El exchange cobrará el monto indicado cada vez que se haga una transferencia.",
+ ],
+ Operation: ["Operación"],
+ "Wallet operations": ["Operaciones de billetera"],
+ Feature: ["Característica"],
+ "Could not get the info from the URI": [
+ "No se pudo obtener la información desde la URI",
+ ],
+ "Could not get info of withdrawal": [
+ "No se pudo obtener la información de retiro",
+ ],
+ "Digital cash withdrawal": ["Retirada de dinero digital"],
+ "Could not finish the withdrawal operation": [
+ "No se pudo completar la operación de retirada",
+ ],
+ "Age restriction": ["Restricción etaria"],
+ "Withdraw &nbsp; %1$s": ["Retirar %1$s"],
+ "Withdraw to a mobile phone": ["Retirar con un teléfono móvil"],
+ "Digital invoice": ["Factura digital"],
+ "Could not finish the invoice creation": [
+ "No se pudo completar la creación de la factura",
+ ],
+ Create: ["Crear"],
+ "Could not finish the payment operation": [
+ "No se pudo completar la operación de pago",
+ ],
+ "Digital cash transfer": ["Transferencia de dinero digital"],
+ "Could not finish the transfer creation": [
+ "No se pudo completar la operación de creación de transferencia",
+ ],
+ "Could not finish the pickup operation": [
+ "No se pudo completar la operación de recolección",
+ ],
+ "Manual Withdrawal for %1$s": ["Retirada Manual para %1$s"],
+ "Choose a exchange from where the coins will be withdrawn. The exchange will send the coins to this wallet after receiving a wire transfer with the correct subject.":
+ [
+ "Elija un exchange desde donde las monedas serán retiradas. El exchange enviará las monedas a esta billetera después de recibir una transferencia bancaria con el asunto correcto.",
+ ],
+ "No exchange found for %1$s": ["No se encontró exchange para %1$s"],
+ "Add Exchange": ["Agregar Exchange"],
+ "No exchange configured": ["Sin exchange configurado"],
+ "Can&apos;t create the reserve": ["No se pudo crear una reserva"],
+ "Start withdrawal": ["Comenzar la retirada"],
+ "Could not load deposit balance": [
+ "No se pudo cargar el balance de depósito",
+ ],
+ "A currency or an amount should be indicated": [
+ "Se debería especificar una divisa o un monto",
+ ],
+ "There is no enough balance to make a deposit for currency %1$s": [
+ "No hay suficiente balance para hacer un depósito para la divisa %1$s",
+ ],
+ "Send %1$s to your account": ["Enviar %1$s a tu cuenta"],
+ "There is no account to make a deposit for currency %1$s": [
+ "No hay una cuenta para hacer un depósito para la divisa %1$s",
+ ],
+ "Add account": ["Agregar cuenta"],
+ "Select account": ["Seleccionar cuenta"],
+ "Add another account": ["Agregar otra cuenta"],
+ "Deposit fee": ["Comisión de depósito"],
+ "Total deposit": ["Depósito total"],
+ "Deposit&nbsp;%1$s %2$s": ["Depositar %1$s %2$s"],
+ "Add bank account for %1$s": ["Agregar cuenta de banco para %1$s"],
+ "Enter the URL of an exchange you trust.": [
+ "Ingresar la URL de un exchange en el que confíes.",
+ ],
+ "Unable add this account": ["No fue posible agregar esta cuenta"],
+ "Select account type": ["Seleccione un tipo de cuenta"],
+ "Review terms of service": ["Revisar los términos de servicio"],
+ "Exchange URL": ["Exchange URL"],
+ "Add exchange": ["Agregar exchange"],
+ "Add new exchange": ["Agregar nuevo exchange"],
+ "Add exchange for %1$s": ["Agregar exchange para %1$s"],
+ "An exchange has been found! Review the information and click next": [
+ "Un exchange ha sido encontrado! Revisa la información y haz clic en siguiente",
+ ],
+ "This exchange doesn&apos;t match the expected currency %1$s": [
+ "Este exchange no coincide con la divisa %1$s esperada",
+ ],
+ "Unable to verify this exchange": [
+ "No fue posible verificar este exchange",
+ ],
+ "Unable to add this exchange": ["No fue posible agregar este exchange"],
+ loading: ["cargando"],
+ Version: ["Versión"],
+ Next: ["Siguiente"],
+ "Waiting for confirmation": ["Esperando confirmación"],
+ PENDING: ["PENDIENTE"],
+ "Could not load the list of transactions": [
+ "No se pudo cargar la lista de transacciones",
+ ],
+ "Your transaction history is empty for this currency.": [
+ "No hay historial para esta divisa.",
+ ],
+ "Add backup provider": ["Agregar proveedor de copias de seguridad"],
+ "Could not get provider information": [
+ "No se pudo conseguir la información del proveedor",
+ ],
+ "Backup providers may charge for their service": [
+ "Los proveedores de copias de seguridad pueden cobrarte por su servicio",
+ ],
+ URL: ["URL"],
+ Name: ["Nombre"],
+ "Provider URL": ["URL del proveedor"],
+ "Please review and accept this provider&apos;s terms of service": [
+ "Por favor revisa y acepta los términos de servicio del proveedor",
+ ],
+ Pricing: ["Precios"],
+ "free of charge": ["Gratis"],
+ "%1$s per year of service": ["%1$s por año de servicio"],
+ Storage: ["Alamcenamiento"],
+ "%1$s megabytes of storage per year of service": [
+ "%1$s megabytes de almacenamiento por año de servicio",
+ ],
+ "Accept terms of service": ["Aceptar los términos de servicio"],
+ "Could not parse the payto URI": [
+ "No se pudo obtener la información de la URI payto",
+ ],
+ "Please check the uri": ["Revisar la URI"],
+ "Exchange is ready for withdrawal": [
+ "El exchange está listo para la retirada",
+ ],
+ "To complete the process you need to wire%1$s %2$s to the exchange bank account":
+ [
+ "Para completar el proceso necesitas transferir %1$s %2$s a la cuenta bancaria del exchange",
+ ],
+ "Alternative, you can also scan this QR code or open %1$s if you have a banking app installed that supports RFC 8905":
+ [
+ "Alternativamente, también puedes escanear el código QR o abrir %1$s si tienes una App bancaria instalada que soporta RFC 8905",
+ ],
+ "Cancel withdrawal": ["Cancelar retirada"],
+ "Could not toggle auto-open": ["No se pudo cambiar el auto-open"],
+ "Could not toggle clipboard": ["No se pudo cambiar portapapeles"],
+ Navigator: ["Navegador"],
+ "Automatically open wallet based on page content": [
+ "Abrir automáticamente la billetera basada en el contenido de la página",
+ ],
+ "Enabling this option below will make using the wallet faster, but requires more permissions from your browser.":
+ [
+ "Habilitar la opción de debajo, hará que el uso de la billetera sea mas rápido, pero requiere más permisos de tu navegador.",
+ ],
+ "Automatically check clipboard for Taler URI": [
+ "Revisar el portapapeles automáticamente por Taler URI",
+ ],
+ Trust: ["Confianza"],
+ "No exchange yet": ["No hay exchanges todavía"],
+ "Term of Service": ["Términos de servicio"],
+ ok: ["ok"],
+ changed: ["modificado"],
+ "not accepted": ["no aceptado"],
+ "unknown (exchange status should be updated)": [
+ "desconocido (el estado del exchange debería actualizarse)",
+ ],
+ "Add an exchange": ["Agregar un exchange"],
+ Troubleshooting: ["Solución de problemas"],
+ "Developer mode": ["Modo desarrollador"],
+ "More options and information useful for debugging": [
+ "Más información y opciones útiles para depuración",
+ ],
+ Display: ["Pantalla"],
+ "Current Language": ["Lenguaje actual"],
+ "Wallet Core": ["Wallet core"],
+ "Web Extension": ["Web Extension"],
+ "Exchange compatibility": ["Compatibilidad con Exchange"],
+ "Merchant compatibility": ["Compatibilidad con Merchant"],
+ "Bank compatibility": ["Compatibilidad con Bank"],
+ "Browser Extension Installed!": ["Extensión del navegador instalada!"],
+ "You can open the GNU Taler Wallet using the combination %1$s .": [
+ "Puedes abrir GNU Taler Wallet usando la combinación %1$s.",
+ ],
+ "Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick access without keyboard:":
+ [
+ "También fijando GNU Taler Wallet a to navegador Chrome permite un acceso rápido sin el teclado:",
+ ],
+ "Click the puzzle icon": ["Haz click en el ícono de rompecabezas"],
+ "Search for GNU Taler Wallet": ['Busca "GNU Taler Wallet"'],
+ "Click the pin icon": ["Haz click en el ícono de fijar"],
+ Permissions: ["Permisos"],
+ "(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)":
+ [
+ "(Habilitar esta opción de abajo hará el uso de la billetera mas rápido, pero requiere mas permisos de tu navegador)",
+ ],
+ "Next Steps": ["Próximos pasos"],
+ "Try the demo": ["Probar la demostración"],
+ "Learn how to top up your wallet balance": [
+ "Aprender como llenar tu billetera",
+ ],
+ "Diagnostics timed out. Could not talk to the wallet backend.": [
+ "El diagnóstico caducó. No nos pudimos comunicar con la billetera.",
+ ],
+ "Problems detected:": ["Problemas detectados:"],
+ "Please check in your %1$s settings that you have IndexedDB enabled (check the preference name %2$s).":
+ [
+ "Por favor revisa en tu configuración %1$s que tienes IndexedDB habilitado (el nombre de la preferencia %2$s).",
+ ],
+ "Your wallet database is outdated. Currently automatic migration is not supported. Please go %1$s to reset the wallet database.":
+ [
+ "La base de datos de la billetera expiró. Por ahora la migración automática no está soportada. Por favor dirijasé a %1$s para reiniciar la base de datos de la billetera.",
+ ],
+ "Running diagnostics": ["Ejecutando diagnósticos"],
+ "Debug tools": ["Herramientas de desarrollo"],
+ "Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?":
+ [
+ "Quieres DESTRUIR IRREVOCABLEMENTE todo dentro de tu billetera y PERDER TODAS TUS MONEDAS?",
+ ],
+ reset: ["Reiniciar"],
+ "TESTING: This may delete all your coin, proceed with caution": [
+ "TESTING: Esto puede borrar todas tus monedas, proceder con precaución",
+ ],
+ "run gc": ["Ejecutar GC"],
+ "import database": ["importar base de datos"],
+ "export database": ["exportar base de datos"],
+ "Database exported at %1$s %2$s to download": [
+ "Base de datos exportada a %1$s %2$s para descargar",
+ ],
+ Coins: ["Monedas"],
+ "Pending operations": ["Operaciones pendientes"],
+ "usable coins": ["monedas usables"],
+ id: ["id"],
+ denom: ["denominación"],
+ value: ["valor"],
+ status: ["estado"],
+ "from refresh?": ["desde refresco?"],
+ "age key count": ["cantidad de age key"],
+ "spent coins": ["monedas gastadas"],
+ "click to show": ["hacer clic para mostrar"],
+ "Scan a QR code or enter taler:// URI below": [
+ "Escanear un código QR o ingresar taler:// URI debajo",
+ ],
+ Open: ["Abrir"],
+ "URI is not valid. Taler URI should start with `taler://`": [
+ "El URI no es válido. Taler URI debería comenzar con `taler://`",
+ ],
+ "Try another": ["Intentar otro"],
+ "Could not load list of exchange": [
+ "No se pudo cargar la lista de exchange",
+ ],
+ "Choose a currency to proceed or add another exchange": [
+ "Elija una divisa para proceder o agregue otro exchange",
+ ],
+ "Known currencies": ["Divisas conocidas"],
+ "Specify the amount and the origin": ["Indicar el monto y el origen"],
+ "Change currency": ["Cambiar divisa"],
+ "Use previous origins:": ["Usar un origen previo:"],
+ "Or specify the origin of the money": [
+ "O especificar el origen del dinero",
+ ],
+ "Specify the origin of the money": ["Especificar el origen del dinero"],
+ "From my bank account": ["Desde mi cuenta de banco"],
+ "From another wallet": ["Desde otra billetera"],
+ "currency not provided": ["Divisa no provista"],
+ "Specify the amount and the destination": [
+ "Especificar el monto y el destino",
+ ],
+ "Use previous destinations:": ["Usar destinos previos:"],
+ "Or specify the destination of the money": [
+ "O especificar el destino del dinero",
+ ],
+ "Specify the destination of the money": [
+ "Especificar el destino del dinero",
+ ],
+ "To my bank account": ["Hacia mi cuenta de banco"],
+ "To another wallet": ["Hacia otra billetera"],
+ "Could not load backup recovery information": [
+ "No se pudo cargar la información de recuperación de copia de seguridad",
+ ],
+ "Digital wallet recovery": ["Recuperación de billetera digital"],
+ "Import backup, show info": [
+ "Importar copia de seguridad, mostrar información",
+ ],
+ "All done, your transaction is in progress": [
+ "Todo completo, su transacción está en progreso",
+ ],
+ Edit: ["Editar"],
+ "Could not load the list of known exchanges": [
+ "No se pudo cargar la lista de exchange conocidos",
+ ],
+ },
+ },
+};
+
+strings["fr"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n!=1);",
+ lang: "fr",
},
- "Invalid Wire": [""],
- "Invalid Test Wire Detail": [""],
- "Test Wire Acct #%1$s on %2$s": [""],
- "Unknown Wire Detail": [""],
- Operation: [""],
- "time (ms/op)": [""],
- "The merchant %1$s offers you to purchase:": [""],
- "The total price is %1$s (plus %2$s fees).": [""],
- "The total price is %1$s.": [""],
- Retry: [""],
- "Confirm payment": [""],
Balance: [""],
- History: [""],
- Debug: [""],
- "You have no balance to show. Need some %1$s getting started?": [""],
- "%1$s incoming": [""],
- "%1$s being spent": [""],
- "Error: could not retrieve balance information.": [""],
- "Invalid ": [""],
- "Fees ": [""],
- "Refresh sessions has completed": [""],
- "Order Refused": [""],
- "Order redirected": [""],
- "Payment aborted": [""],
- "Payment Sent": [""],
- "Order accepted": [""],
- "Reserve balance updated": [""],
- "Payment refund": [""],
- Withdrawn: [""],
- "Tip Accepted": [""],
- "Tip Declined": [""],
+ Backup: [""],
+ "QR Reader and Taler URI": [""],
+ Settings: [""],
+ Dev: [""],
"%1$s": [""],
- "Your wallet has no events recorded.": [""],
- "Wire to bank account": [""],
- Confirm: [""],
- Cancel: [""],
- "Could not get details for withdraw operation:": [""],
- "Chose different exchange provider": [""],
- "Please select an exchange. You can review the details before after your selection.": [
+ "PENDING OPERATIONS": [""],
+ Loading: [""],
+ "Could not load backup providers": [""],
+ "No backup providers configured": [""],
+ "Add provider": [""],
+ "Sync all backups": [""],
+ "Sync now": [""],
+ "Last synced": [""],
+ "Not synced": [""],
+ "Expires in": [""],
+ "There was an error loading the provider detail for &quot; %1$s&quot;": [
"",
],
- "Select %1$s": [""],
- "Select custom exchange": [""],
- "You are about to withdraw %1$s from your bank account into your wallet.": [
+ "There is not known provider with url &quot;%1$s&quot;.": [""],
+ "See providers": [""],
+ "Last backup": [""],
+ "Back up": [""],
+ "Provider fee": [""],
+ "per year": [""],
+ Extend: [""],
+ "terms has changed, extending the service will imply accepting the new terms of service":
+ [""],
+ old: [""],
+ new: [""],
+ fee: [""],
+ storage: [""],
+ "Remove provider": [""],
+ "This provider has reported an error": [""],
+ "There is conflict with another backup from %1$s": [""],
+ "Backup is not readable": [""],
+ "Unknown backup problem: %1$s": [""],
+ "service paid": [""],
+ "Backup valid until": [""],
+ Cancel: [""],
+ "Open reserve page": [""],
+ "Open pay page": [""],
+ "Open refund page": [""],
+ "Open tip page": [""],
+ "Open withdraw page": [""],
+ "Get digital cash": [""],
+ "Could not load balance page": [""],
+ Add: [""],
+ "Send %1$s": [""],
+ "Taler Action": [""],
+ "This page has pay action.": [""],
+ "This page has a withdrawal action.": [""],
+ "This page has a tip action.": [""],
+ "This page has a notify reserve action.": [""],
+ Notify: [""],
+ "This page has a refund action.": [""],
+ "This page has a malformed taler uri.": [""],
+ Dismiss: [""],
+ "this popup is being closed and you are being redirected to %1$s": [""],
+ "Could not load purchase proposal details": [""],
+ "Order Id": [""],
+ Summary: [""],
+ Amount: [""],
+ "Merchant name": [""],
+ "Merchant jurisdiction": [""],
+ "Merchant address": [""],
+ "Merchant logo": [""],
+ "Merchant website": [""],
+ "Merchant email": [""],
+ "Merchant public key": [""],
+ "Delivery date": [""],
+ "Delivery location": [""],
+ Products: [""],
+ "Created at": [""],
+ "Refund deadline": [""],
+ "Auto refund": [""],
+ "Pay deadline": [""],
+ "Fulfillment URL": [""],
+ "Fulfillment message": [""],
+ "Max deposit fee": [""],
+ "Max fee": [""],
+ "Minimum age": [""],
+ "Wire fee amortization": [""],
+ Auditors: [""],
+ Exchanges: [""],
+ "Bank account": [""],
+ "Bitcoin address": [""],
+ IBAN: [""],
+ "Could not load deposit status": [""],
+ "Digital cash deposit": [""],
+ Cost: [""],
+ Fee: [""],
+ "To be received": [""],
+ "Send &nbsp; %1$s": [""],
+ "Bitcoin transfer details": [""],
+ "The exchange need a transaction with 3 output, one output is the exchange account and the other two are segwit fake address for metadata with an minimum amount.":
+ [""],
+ "In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional recipient and copy addresses and amounts":
+ [""],
+ "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC":
+ [""],
+ Account: [""],
+ "Bank host": [""],
+ "Bank transfer details": [""],
+ Subject: [""],
+ "Receiver name": [""],
+ "Could not load the transaction information": [""],
+ "There was an error trying to complete the transaction": [""],
+ "This transaction is not completed": [""],
+ Send: [""],
+ Retry: [""],
+ Forget: [""],
+ "Caution!": [""],
+ "If you have already wired money to the exchange you will loose the chance to get the coins form it.":
+ [""],
+ Confirm: ["Confirmer"],
+ Withdrawal: [""],
+ "Make sure to use the correct subject, otherwise the money will not arrive in this wallet.":
+ [""],
+ "The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check there is no pending step.":
+ [""],
+ "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins":
+ [""],
+ Details: [""],
+ Payment: [""],
+ Refunds: [""],
+ "%1$s %2$s on %3$s": [""],
+ "Merchant created a refund for this order but was not automatically picked up.":
+ [""],
+ Offer: [""],
+ Accept: [""],
+ Merchant: [""],
+ "Invoice ID": [""],
+ Deposit: [""],
+ Refresh: [""],
+ Tip: [""],
+ Refund: [""],
+ "Original order ID": [""],
+ "Purchase summary": [""],
+ copy: [""],
+ "hide qr": [""],
+ "show qr": [""],
+ Credit: [""],
+ Invoice: [""],
+ Exchange: [""],
+ URI: [""],
+ Debit: [""],
+ Transfer: [""],
+ Country: [""],
+ "Address lines": [""],
+ "Building number": [""],
+ "Building name": [""],
+ Street: [""],
+ "Post code": [""],
+ "Town location": [""],
+ Town: [""],
+ District: [""],
+ "Country subdivision": [""],
+ Date: [""],
+ "Transaction fees": [""],
+ Total: [""],
+ Withdraw: [""],
+ Price: [""],
+ Refunded: [""],
+ Delivery: [""],
+ "Total transfer": [""],
+ "Could not load pay status": [""],
+ "Digital cash payment": [""],
+ Purchase: [""],
+ Receipt: [""],
+ "Valid until": [""],
+ "List of products": [""],
+ free: [""],
+ "Already paid, you are going to be redirected to %1$s": [""],
+ "Already paid": [""],
+ "Already claimed": [""],
+ "Pay with a mobile phone": [""],
+ "Hide QR": [""],
+ "Scan the QR code or &nbsp; %1$s": [""],
+ "Pay &nbsp; %1$s": [""],
+ "You have no balance for this currency. Withdraw digital cash first.": [
"",
],
- "Accept fees and withdraw": [""],
- "Cancel withdraw operation": [""],
- "Withdrawal fees:": [""],
- "Rounding loss:": [""],
- "Earliest expiration (for deposit): %1$s": [""],
- "# Coins": [""],
- Value: [""],
- "Withdraw Fee": [""],
- "Refresh Fee": [""],
- "Deposit Fee": [""],
+ "Could not find enough coins to pay. Even if you have enough %1$s some restriction may apply.":
+ [""],
+ "Your current balance is not enough.": [""],
+ "Merchant message": [""],
+ "Could not load refund status": [""],
+ "Digital cash refund": [""],
+ "You&apos;ve ignored the tip.": [""],
+ "The refund is in progress.": [""],
+ "Total to refund": [""],
+ "The merchant &quot;%1$s&quot; is offering you a refund.": [""],
+ "Order amount": [""],
+ "Already refunded": [""],
+ "Refund offered": [""],
+ "Accept &nbsp; %1$s": [""],
+ "Could not load tip status": [""],
+ "Digital cash tip": [""],
+ "The merchant is offering you a tip": [""],
+ "Merchant URL": [""],
+ "Receive &nbsp; %1$s": [""],
+ "Tip from %1$s accepted. Check your transactions list for more details.":
+ [""],
+ "Select one option": [""],
+ "Could not load": [""],
+ "Show terms of service": [""],
+ "I accept the exchange terms of service": [""],
+ "Exchange doesn&apos;t have terms of service": [""],
+ "Review exchange terms of service": [""],
+ "Review new version of terms of service": [""],
+ "The exchange reply with a empty terms of service": [""],
+ "Download Terms of Service": [""],
+ "Hide terms of service": [""],
+ "Could not load exchange fees": [""],
+ Close: [""],
+ "could not find any exchange": [""],
+ "could not find any exchange for the currency %1$s": [""],
+ "Service fee description": [""],
+ "Select %1$s exchange": [""],
+ Reset: [""],
+ "Use this exchange": [""],
+ "Doesn&apos;t have auditors": [""],
+ currency: [""],
+ Operations: [""],
+ Deposits: [""],
+ Denomination: [""],
+ Until: [""],
+ Withdrawals: [""],
+ Currency: [""],
+ "Coin operations": [""],
+ "Every operation in this section may be different by denomination value and is valid for a period of time. The exchange will charge the indicated amount every time a coin is used in such operation.":
+ [""],
+ "Transfer operations": [""],
+ "Every operation in this section may be different by transfer type and is valid for a period of time. The exchange will charge the indicated amount every time a transfer is made.":
+ [""],
+ Operation: [""],
+ "Wallet operations": [""],
+ Feature: [""],
+ "Could not get the info from the URI": [""],
+ "Could not get info of withdrawal": [""],
+ "Digital cash withdrawal": [""],
+ "Could not finish the withdrawal operation": [""],
+ "Age restriction": [""],
+ "Withdraw &nbsp; %1$s": [""],
+ "Withdraw to a mobile phone": [""],
+ "Digital invoice": [""],
+ "Could not finish the invoice creation": [""],
+ Create: [""],
+ "Could not finish the payment operation": [""],
+ "Digital cash transfer": [""],
+ "Could not finish the transfer creation": [""],
+ "Could not finish the pickup operation": [""],
+ "Manual Withdrawal for %1$s": [""],
+ "Choose a exchange from where the coins will be withdrawn. The exchange will send the coins to this wallet after receiving a wire transfer with the correct subject.":
+ [""],
+ "No exchange found for %1$s": [""],
+ "Add Exchange": [""],
+ "No exchange configured": [""],
+ "Can&apos;t create the reserve": [""],
+ "Start withdrawal": [""],
+ "Could not load deposit balance": [""],
+ "A currency or an amount should be indicated": [""],
+ "There is no enough balance to make a deposit for currency %1$s": [""],
+ "Send %1$s to your account": [""],
+ "There is no account to make a deposit for currency %1$s": [""],
+ "Add account": [""],
+ "Select account": [""],
+ "Add another account": [""],
+ "Deposit fee": [""],
+ "Total deposit": [""],
+ "Deposit&nbsp;%1$s %2$s": [""],
+ "Add bank account for %1$s": [""],
+ "Enter the URL of an exchange you trust.": [""],
+ "Unable add this account": [""],
+ "Select account type": [""],
+ "Review terms of service": [""],
+ "Exchange URL": [""],
+ "Add exchange": [""],
+ "Add new exchange": [""],
+ "Add exchange for %1$s": [""],
+ "An exchange has been found! Review the information and click next": [""],
+ "This exchange doesn&apos;t match the expected currency %1$s": [""],
+ "Unable to verify this exchange": [""],
+ "Unable to add this exchange": [""],
+ loading: [""],
+ Version: [""],
+ Next: [""],
+ "Waiting for confirmation": [""],
+ PENDING: [""],
+ "Could not load the list of transactions": [""],
+ "Your transaction history is empty for this currency.": [""],
+ "Add backup provider": [""],
+ "Could not get provider information": [""],
+ "Backup providers may charge for their service": [""],
+ URL: [""],
+ Name: [""],
+ "Provider URL": [""],
+ "Please review and accept this provider&apos;s terms of service": [""],
+ Pricing: [""],
+ "free of charge": [""],
+ "%1$s per year of service": [""],
+ Storage: [""],
+ "%1$s megabytes of storage per year of service": [""],
+ "Accept terms of service": [""],
+ "Could not parse the payto URI": [""],
+ "Please check the uri": [""],
+ "Exchange is ready for withdrawal": [""],
+ "To complete the process you need to wire%1$s %2$s to the exchange bank account":
+ [""],
+ "Alternative, you can also scan this QR code or open %1$s if you have a banking app installed that supports RFC 8905":
+ [""],
+ "Cancel withdrawal": [""],
+ "Could not toggle auto-open": [""],
+ "Could not toggle clipboard": [""],
+ Navigator: [""],
+ "Automatically open wallet based on page content": [""],
+ "Enabling this option below will make using the wallet faster, but requires more permissions from your browser.":
+ [""],
+ "Automatically check clipboard for Taler URI": [""],
+ Trust: [""],
+ "No exchange yet": [""],
+ "Term of Service": [""],
+ ok: [""],
+ changed: [""],
+ "not accepted": [""],
+ "unknown (exchange status should be updated)": [""],
+ "Add an exchange": [""],
+ Troubleshooting: [""],
+ "Developer mode": [""],
+ "More options and information useful for debugging": [""],
+ Display: [""],
+ "Current Language": [""],
+ "Wallet Core": [""],
+ "Web Extension": [""],
+ "Exchange compatibility": [""],
+ "Merchant compatibility": [""],
+ "Bank compatibility": [""],
+ "Browser Extension Installed!": [""],
+ "You can open the GNU Taler Wallet using the combination %1$s .": [""],
+ "Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick access without keyboard:":
+ [""],
+ "Click the puzzle icon": [""],
+ "Search for GNU Taler Wallet": [""],
+ "Click the pin icon": [""],
+ Permissions: [""],
+ "(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)":
+ [""],
+ "Next Steps": [""],
+ "Try the demo": [""],
+ "Learn how to top up your wallet balance": [""],
+ "Diagnostics timed out. Could not talk to the wallet backend.": [""],
+ "Problems detected:": [""],
+ "Please check in your %1$s settings that you have IndexedDB enabled (check the preference name %2$s).":
+ [""],
+ "Your wallet database is outdated. Currently automatic migration is not supported. Please go %1$s to reset the wallet database.":
+ [""],
+ "Running diagnostics": [""],
+ "Debug tools": [""],
+ "Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?":
+ [""],
+ reset: [""],
+ "TESTING: This may delete all your coin, proceed with caution": [""],
+ "run gc": [""],
+ "import database": [""],
+ "export database": [""],
+ "Database exported at %1$s %2$s to download": [""],
+ Coins: [""],
+ "Pending operations": [""],
+ "usable coins": [""],
+ id: [""],
+ denom: [""],
+ value: [""],
+ status: [""],
+ "from refresh?": [""],
+ "age key count": [""],
+ "spent coins": [""],
+ "click to show": [""],
+ "Scan a QR code or enter taler:// URI below": [""],
+ Open: [""],
+ "URI is not valid. Taler URI should start with `taler://`": [""],
+ "Try another": [""],
+ "Could not load list of exchange": [""],
+ "Choose a currency to proceed or add another exchange": [""],
+ "Known currencies": [""],
+ "Specify the amount and the origin": [""],
+ "Change currency": [""],
+ "Use previous origins:": [""],
+ "Or specify the origin of the money": [""],
+ "Specify the origin of the money": [""],
+ "From my bank account": [""],
+ "From another wallet": [""],
+ "currency not provided": [""],
+ "Specify the amount and the destination": [""],
+ "Use previous destinations:": [""],
+ "Or specify the destination of the money": [""],
+ "Specify the destination of the money": [""],
+ "To my bank account": [""],
+ "To another wallet": [""],
+ "Could not load backup recovery information": [""],
+ "Digital wallet recovery": [""],
+ "Import backup, show info": [""],
+ "All done, your transaction is in progress": [""],
+ Edit: [""],
+ "Could not load the list of known exchanges": [""],
},
},
};
-strings["es"] = {
+strings["it"] = {
domain: "messages",
locale_data: {
messages: {
"": {
domain: "messages",
- plural_forms: "nplurals=2; plural=(n != 1);",
- lang: "",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
},
- "Invalid Wire": [""],
- "Invalid Test Wire Detail": [""],
- "Test Wire Acct #%1$s on %2$s": [""],
- "Unknown Wire Detail": [""],
- Operation: [""],
- "time (ms/op)": [""],
- "The merchant %1$s offers you to purchase:": [""],
- "The total price is %1$s (plus %2$s fees).": [""],
- "The total price is %1$s.": [""],
- Retry: [""],
- "Confirm payment": [""],
Balance: [""],
- History: ["Historial"],
- Debug: [""],
- "You have no balance to show. Need some %1$s getting started?": [""],
- "%1$s incoming": [""],
- "%1$s being spent": [""],
- "Error: could not retrieve balance information.": [""],
- "Invalid ": [""],
- "Fees ": [""],
- "Refresh sessions has completed": [""],
- "Order Refused": [""],
- "Order redirected": [""],
- "Payment aborted": [""],
- "Payment Sent": [""],
- "Backup": ["Resguardo"],
- "Order accepted": [""],
- "Reserve balance updated": [""],
- "Payment refund": [""],
- Withdrawn: [""],
- "Tip Accepted": [""],
- "Tip Declined": [""],
+ Backup: [""],
+ "QR Reader and Taler URI": [""],
+ Settings: [""],
+ Dev: [""],
"%1$s": [""],
- "Your wallet has no events recorded.": [""],
- "Wire to bank account": [""],
- Confirm: ["Confirmar"],
- Cancel: ["Cancelar"],
- "Could not get details for withdraw operation:": [""],
- "Chose different exchange provider": [""],
- "Please select an exchange. You can review the details before after your selection.": [
+ "PENDING OPERATIONS": [""],
+ Loading: [""],
+ "Could not load backup providers": [""],
+ "No backup providers configured": [""],
+ "Add provider": [""],
+ "Sync all backups": [""],
+ "Sync now": [""],
+ "Last synced": [""],
+ "Not synced": [""],
+ "Expires in": [""],
+ "There was an error loading the provider detail for &quot; %1$s&quot;": [
"",
],
- "Select %1$s": [""],
- "Select custom exchange": [""],
- "You are about to withdraw %1$s from your bank account into your wallet.": [
+ "There is not known provider with url &quot;%1$s&quot;.": [""],
+ "See providers": [""],
+ "Last backup": [""],
+ "Back up": [""],
+ "Provider fee": [""],
+ "per year": [""],
+ Extend: [""],
+ "terms has changed, extending the service will imply accepting the new terms of service":
+ [""],
+ old: [""],
+ new: [""],
+ fee: [""],
+ storage: [""],
+ "Remove provider": [""],
+ "This provider has reported an error": [""],
+ "There is conflict with another backup from %1$s": [""],
+ "Backup is not readable": [""],
+ "Unknown backup problem: %1$s": [""],
+ "service paid": [""],
+ "Backup valid until": [""],
+ Cancel: [""],
+ "Open reserve page": [""],
+ "Open pay page": [""],
+ "Open refund page": [""],
+ "Open tip page": [""],
+ "Open withdraw page": [""],
+ "Get digital cash": [""],
+ "Could not load balance page": [""],
+ Add: [""],
+ "Send %1$s": [""],
+ "Taler Action": [""],
+ "This page has pay action.": [""],
+ "This page has a withdrawal action.": [""],
+ "This page has a tip action.": [""],
+ "This page has a notify reserve action.": [""],
+ Notify: [""],
+ "This page has a refund action.": [""],
+ "This page has a malformed taler uri.": [""],
+ Dismiss: [""],
+ "this popup is being closed and you are being redirected to %1$s": [""],
+ "Could not load purchase proposal details": [""],
+ "Order Id": [""],
+ Summary: [""],
+ Amount: [""],
+ "Merchant name": [""],
+ "Merchant jurisdiction": [""],
+ "Merchant address": [""],
+ "Merchant logo": [""],
+ "Merchant website": [""],
+ "Merchant email": [""],
+ "Merchant public key": [""],
+ "Delivery date": [""],
+ "Delivery location": [""],
+ Products: [""],
+ "Created at": [""],
+ "Refund deadline": [""],
+ "Auto refund": [""],
+ "Pay deadline": [""],
+ "Fulfillment URL": [""],
+ "Fulfillment message": [""],
+ "Max deposit fee": [""],
+ "Max fee": [""],
+ "Minimum age": [""],
+ "Wire fee amortization": [""],
+ Auditors: [""],
+ Exchanges: [""],
+ "Bank account": [""],
+ "Bitcoin address": [""],
+ IBAN: [""],
+ "Could not load deposit status": [""],
+ "Digital cash deposit": [""],
+ Cost: [""],
+ Fee: [""],
+ "To be received": [""],
+ "Send &nbsp; %1$s": [""],
+ "Bitcoin transfer details": [""],
+ "The exchange need a transaction with 3 output, one output is the exchange account and the other two are segwit fake address for metadata with an minimum amount.":
+ [""],
+ "In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional recipient and copy addresses and amounts":
+ [""],
+ "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC":
+ [""],
+ Account: [""],
+ "Bank host": [""],
+ "Bank transfer details": [""],
+ Subject: [""],
+ "Receiver name": [""],
+ "Could not load the transaction information": [""],
+ "There was an error trying to complete the transaction": [""],
+ "This transaction is not completed": [""],
+ Send: [""],
+ Retry: [""],
+ Forget: [""],
+ "Caution!": [""],
+ "If you have already wired money to the exchange you will loose the chance to get the coins form it.":
+ [""],
+ Confirm: ["Confermare"],
+ Withdrawal: [""],
+ "Make sure to use the correct subject, otherwise the money will not arrive in this wallet.":
+ [""],
+ "The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check there is no pending step.":
+ [""],
+ "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins":
+ [""],
+ Details: [""],
+ Payment: [""],
+ Refunds: [""],
+ "%1$s %2$s on %3$s": [""],
+ "Merchant created a refund for this order but was not automatically picked up.":
+ [""],
+ Offer: [""],
+ Accept: [""],
+ Merchant: [""],
+ "Invoice ID": [""],
+ Deposit: [""],
+ Refresh: [""],
+ Tip: [""],
+ Refund: [""],
+ "Original order ID": [""],
+ "Purchase summary": [""],
+ copy: [""],
+ "hide qr": [""],
+ "show qr": [""],
+ Credit: [""],
+ Invoice: [""],
+ Exchange: [""],
+ URI: [""],
+ Debit: [""],
+ Transfer: [""],
+ Country: [""],
+ "Address lines": [""],
+ "Building number": [""],
+ "Building name": [""],
+ Street: [""],
+ "Post code": [""],
+ "Town location": [""],
+ Town: [""],
+ District: [""],
+ "Country subdivision": [""],
+ Date: [""],
+ "Transaction fees": [""],
+ Total: [""],
+ Withdraw: [""],
+ Price: [""],
+ Refunded: [""],
+ Delivery: [""],
+ "Total transfer": [""],
+ "Could not load pay status": [""],
+ "Digital cash payment": [""],
+ Purchase: [""],
+ Receipt: [""],
+ "Valid until": [""],
+ "List of products": [""],
+ free: [""],
+ "Already paid, you are going to be redirected to %1$s": [""],
+ "Already paid": [""],
+ "Already claimed": [""],
+ "Pay with a mobile phone": [""],
+ "Hide QR": [""],
+ "Scan the QR code or &nbsp; %1$s": [""],
+ "Pay &nbsp; %1$s": [""],
+ "You have no balance for this currency. Withdraw digital cash first.": [
"",
],
- "Accept fees and withdraw": [""],
- "Cancel withdraw operation": [""],
- "Withdrawal fees:": [""],
- "Rounding loss:": [""],
- "Earliest expiration (for deposit): %1$s": [""],
- "# Coins": [""],
- Value: [""],
- "Withdraw Fee": [""],
- "Refresh Fee": [""],
- "Deposit Fee": [""],
+ "Could not find enough coins to pay. Even if you have enough %1$s some restriction may apply.":
+ [""],
+ "Your current balance is not enough.": [""],
+ "Merchant message": [""],
+ "Could not load refund status": [""],
+ "Digital cash refund": [""],
+ "You&apos;ve ignored the tip.": [""],
+ "The refund is in progress.": [""],
+ "Total to refund": [""],
+ "The merchant &quot;%1$s&quot; is offering you a refund.": [""],
+ "Order amount": [""],
+ "Already refunded": [""],
+ "Refund offered": [""],
+ "Accept &nbsp; %1$s": [""],
+ "Could not load tip status": [""],
+ "Digital cash tip": [""],
+ "The merchant is offering you a tip": [""],
+ "Merchant URL": [""],
+ "Receive &nbsp; %1$s": [""],
+ "Tip from %1$s accepted. Check your transactions list for more details.":
+ [""],
+ "Select one option": [""],
+ "Could not load": [""],
+ "Show terms of service": [""],
+ "I accept the exchange terms of service": [""],
+ "Exchange doesn&apos;t have terms of service": [""],
+ "Review exchange terms of service": [""],
+ "Review new version of terms of service": [""],
+ "The exchange reply with a empty terms of service": [""],
+ "Download Terms of Service": [""],
+ "Hide terms of service": [""],
+ "Could not load exchange fees": [""],
+ Close: [""],
+ "could not find any exchange": [""],
+ "could not find any exchange for the currency %1$s": [""],
+ "Service fee description": [""],
+ "Select %1$s exchange": [""],
+ Reset: [""],
+ "Use this exchange": [""],
+ "Doesn&apos;t have auditors": [""],
+ currency: [""],
+ Operations: [""],
+ Deposits: [""],
+ Denomination: [""],
+ Until: [""],
+ Withdrawals: [""],
+ Currency: [""],
+ "Coin operations": [""],
+ "Every operation in this section may be different by denomination value and is valid for a period of time. The exchange will charge the indicated amount every time a coin is used in such operation.":
+ [""],
+ "Transfer operations": [""],
+ "Every operation in this section may be different by transfer type and is valid for a period of time. The exchange will charge the indicated amount every time a transfer is made.":
+ [""],
+ Operation: [""],
+ "Wallet operations": [""],
+ Feature: [""],
+ "Could not get the info from the URI": [""],
+ "Could not get info of withdrawal": [""],
+ "Digital cash withdrawal": [""],
+ "Could not finish the withdrawal operation": [""],
+ "Age restriction": [""],
+ "Withdraw &nbsp; %1$s": [""],
+ "Withdraw to a mobile phone": [""],
+ "Digital invoice": [""],
+ "Could not finish the invoice creation": [""],
+ Create: [""],
+ "Could not finish the payment operation": [""],
+ "Digital cash transfer": [""],
+ "Could not finish the transfer creation": [""],
+ "Could not finish the pickup operation": [""],
+ "Manual Withdrawal for %1$s": [""],
+ "Choose a exchange from where the coins will be withdrawn. The exchange will send the coins to this wallet after receiving a wire transfer with the correct subject.":
+ [""],
+ "No exchange found for %1$s": [""],
+ "Add Exchange": [""],
+ "No exchange configured": [""],
+ "Can&apos;t create the reserve": [""],
+ "Start withdrawal": [""],
+ "Could not load deposit balance": [""],
+ "A currency or an amount should be indicated": [""],
+ "There is no enough balance to make a deposit for currency %1$s": [""],
+ "Send %1$s to your account": [""],
+ "There is no account to make a deposit for currency %1$s": [""],
+ "Add account": [""],
+ "Select account": [""],
+ "Add another account": [""],
+ "Deposit fee": [""],
+ "Total deposit": [""],
+ "Deposit&nbsp;%1$s %2$s": [""],
+ "Add bank account for %1$s": [""],
+ "Enter the URL of an exchange you trust.": [""],
+ "Unable add this account": [""],
+ "Select account type": [""],
+ "Review terms of service": [""],
+ "Exchange URL": [""],
+ "Add exchange": [""],
+ "Add new exchange": [""],
+ "Add exchange for %1$s": [""],
+ "An exchange has been found! Review the information and click next": [""],
+ "This exchange doesn&apos;t match the expected currency %1$s": [""],
+ "Unable to verify this exchange": [""],
+ "Unable to add this exchange": [""],
+ loading: [""],
+ Version: [""],
+ Next: [""],
+ "Waiting for confirmation": [""],
+ PENDING: [""],
+ "Could not load the list of transactions": [""],
+ "Your transaction history is empty for this currency.": [""],
+ "Add backup provider": [""],
+ "Could not get provider information": [""],
+ "Backup providers may charge for their service": [""],
+ URL: [""],
+ Name: [""],
+ "Provider URL": [""],
+ "Please review and accept this provider&apos;s terms of service": [""],
+ Pricing: [""],
+ "free of charge": [""],
+ "%1$s per year of service": [""],
+ Storage: [""],
+ "%1$s megabytes of storage per year of service": [""],
+ "Accept terms of service": [""],
+ "Could not parse the payto URI": [""],
+ "Please check the uri": [""],
+ "Exchange is ready for withdrawal": [""],
+ "To complete the process you need to wire%1$s %2$s to the exchange bank account":
+ [""],
+ "Alternative, you can also scan this QR code or open %1$s if you have a banking app installed that supports RFC 8905":
+ [""],
+ "Cancel withdrawal": [""],
+ "Could not toggle auto-open": [""],
+ "Could not toggle clipboard": [""],
+ Navigator: [""],
+ "Automatically open wallet based on page content": [""],
+ "Enabling this option below will make using the wallet faster, but requires more permissions from your browser.":
+ [""],
+ "Automatically check clipboard for Taler URI": [""],
+ Trust: [""],
+ "No exchange yet": [""],
+ "Term of Service": [""],
+ ok: [""],
+ changed: [""],
+ "not accepted": [""],
+ "unknown (exchange status should be updated)": [""],
+ "Add an exchange": [""],
+ Troubleshooting: [""],
+ "Developer mode": [""],
+ "More options and information useful for debugging": [""],
+ Display: [""],
+ "Current Language": [""],
+ "Wallet Core": [""],
+ "Web Extension": [""],
+ "Exchange compatibility": [""],
+ "Merchant compatibility": [""],
+ "Bank compatibility": [""],
+ "Browser Extension Installed!": [""],
+ "You can open the GNU Taler Wallet using the combination %1$s .": [""],
+ "Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick access without keyboard:":
+ [""],
+ "Click the puzzle icon": [""],
+ "Search for GNU Taler Wallet": [""],
+ "Click the pin icon": [""],
+ Permissions: [""],
+ "(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)":
+ [""],
+ "Next Steps": [""],
+ "Try the demo": [""],
+ "Learn how to top up your wallet balance": [""],
+ "Diagnostics timed out. Could not talk to the wallet backend.": [""],
+ "Problems detected:": [""],
+ "Please check in your %1$s settings that you have IndexedDB enabled (check the preference name %2$s).":
+ [""],
+ "Your wallet database is outdated. Currently automatic migration is not supported. Please go %1$s to reset the wallet database.":
+ [""],
+ "Running diagnostics": [""],
+ "Debug tools": [""],
+ "Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?":
+ [""],
+ reset: [""],
+ "TESTING: This may delete all your coin, proceed with caution": [""],
+ "run gc": [""],
+ "import database": [""],
+ "export database": [""],
+ "Database exported at %1$s %2$s to download": [""],
+ Coins: [""],
+ "Pending operations": [""],
+ "usable coins": [""],
+ id: [""],
+ denom: [""],
+ value: [""],
+ status: [""],
+ "from refresh?": [""],
+ "age key count": [""],
+ "spent coins": [""],
+ "click to show": [""],
+ "Scan a QR code or enter taler:// URI below": [""],
+ Open: [""],
+ "URI is not valid. Taler URI should start with `taler://`": [""],
+ "Try another": [""],
+ "Could not load list of exchange": [""],
+ "Choose a currency to proceed or add another exchange": [""],
+ "Known currencies": [""],
+ "Specify the amount and the origin": [""],
+ "Change currency": [""],
+ "Use previous origins:": [""],
+ "Or specify the origin of the money": [""],
+ "Specify the origin of the money": [""],
+ "From my bank account": [""],
+ "From another wallet": [""],
+ "currency not provided": [""],
+ "Specify the amount and the destination": [""],
+ "Use previous destinations:": [""],
+ "Or specify the destination of the money": [""],
+ "Specify the destination of the money": [""],
+ "To my bank account": [""],
+ "To another wallet": [""],
+ "Could not load backup recovery information": [""],
+ "Digital wallet recovery": [""],
+ "Import backup, show info": [""],
+ "All done, your transaction is in progress": [""],
+ Edit: [""],
+ "Could not load the list of known exchanges": [""],
},
},
};
-strings["fr"] = {
+strings["ja"] = {
domain: "messages",
locale_data: {
messages: {
"": {
domain: "messages",
- plural_forms: "nplurals=2; plural=(n != 1);",
- lang: "",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "ja",
},
- "Invalid Wire": [""],
- "Invalid Test Wire Detail": [""],
- "Test Wire Acct #%1$s on %2$s": [""],
- "Unknown Wire Detail": [""],
- Operation: [""],
- "time (ms/op)": [""],
- "The merchant %1$s offers you to purchase:": [""],
- "The total price is %1$s (plus %2$s fees).": [""],
- "The total price is %1$s.": [""],
- Retry: [""],
- "Confirm payment": [""],
- Balance: [""],
- History: [""],
- Debug: [""],
- "You have no balance to show. Need some %1$s getting started?": [""],
- "%1$s incoming": [""],
- "%1$s being spent": [""],
- "Error: could not retrieve balance information.": [""],
- "Invalid ": [""],
- "Fees ": [""],
- "Refresh sessions has completed": [""],
- "Order Refused": [""],
- "Order redirected": [""],
- "Payment aborted": [""],
- "Payment Sent": [""],
- "Order accepted": [""],
- "Reserve balance updated": [""],
- "Payment refund": [""],
- Withdrawn: [""],
- "Tip Accepted": [""],
- "Tip Declined": [""],
+ Balance: ["残高"],
+ Backup: ["バックアップ"],
+ "QR Reader and Taler URI": [""],
+ Settings: ["設定"],
+ Dev: [""],
"%1$s": [""],
- "Your wallet has no events recorded.": [""],
- "Wire to bank account": [""],
- Confirm: [""],
- Cancel: [""],
- "Could not get details for withdraw operation:": [""],
- "Chose different exchange provider": [""],
- "Please select an exchange. You can review the details before after your selection.": [
+ "PENDING OPERATIONS": [""],
+ Loading: [""],
+ "Could not load backup providers": [""],
+ "No backup providers configured": [""],
+ "Add provider": [""],
+ "Sync all backups": [""],
+ "Sync now": [""],
+ "Last synced": [""],
+ "Not synced": [""],
+ "Expires in": [""],
+ "There was an error loading the provider detail for &quot; %1$s&quot;": [
"",
],
- "Select %1$s": [""],
- "Select custom exchange": [""],
- "You are about to withdraw %1$s from your bank account into your wallet.": [
+ "There is not known provider with url &quot;%1$s&quot;.": [""],
+ "See providers": [""],
+ "Last backup": [""],
+ "Back up": [""],
+ "Provider fee": [""],
+ "per year": [""],
+ Extend: [""],
+ "terms has changed, extending the service will imply accepting the new terms of service":
+ [""],
+ old: [""],
+ new: [""],
+ fee: [""],
+ storage: [""],
+ "Remove provider": [""],
+ "This provider has reported an error": [""],
+ "There is conflict with another backup from %1$s": [""],
+ "Backup is not readable": [""],
+ "Unknown backup problem: %1$s": [""],
+ "service paid": [""],
+ "Backup valid until": [""],
+ Cancel: [""],
+ "Open reserve page": [""],
+ "Open pay page": [""],
+ "Open refund page": [""],
+ "Open tip page": [""],
+ "Open withdraw page": [""],
+ "Get digital cash": [""],
+ "Could not load balance page": [""],
+ Add: [""],
+ "Send %1$s": [""],
+ "Taler Action": [""],
+ "This page has pay action.": [""],
+ "This page has a withdrawal action.": [""],
+ "This page has a tip action.": [""],
+ "This page has a notify reserve action.": [""],
+ Notify: [""],
+ "This page has a refund action.": [""],
+ "This page has a malformed taler uri.": [""],
+ Dismiss: [""],
+ "this popup is being closed and you are being redirected to %1$s": [""],
+ "Could not load purchase proposal details": [""],
+ "Order Id": [""],
+ Summary: [""],
+ Amount: [""],
+ "Merchant name": [""],
+ "Merchant jurisdiction": [""],
+ "Merchant address": [""],
+ "Merchant logo": [""],
+ "Merchant website": [""],
+ "Merchant email": [""],
+ "Merchant public key": [""],
+ "Delivery date": [""],
+ "Delivery location": [""],
+ Products: [""],
+ "Created at": [""],
+ "Refund deadline": [""],
+ "Auto refund": [""],
+ "Pay deadline": [""],
+ "Fulfillment URL": [""],
+ "Fulfillment message": [""],
+ "Max deposit fee": [""],
+ "Max fee": [""],
+ "Minimum age": [""],
+ "Wire fee amortization": [""],
+ Auditors: [""],
+ Exchanges: [""],
+ "Bank account": [""],
+ "Bitcoin address": [""],
+ IBAN: [""],
+ "Could not load deposit status": [""],
+ "Digital cash deposit": [""],
+ Cost: [""],
+ Fee: [""],
+ "To be received": [""],
+ "Send &nbsp; %1$s": [""],
+ "Bitcoin transfer details": [""],
+ "The exchange need a transaction with 3 output, one output is the exchange account and the other two are segwit fake address for metadata with an minimum amount.":
+ [""],
+ "In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional recipient and copy addresses and amounts":
+ [""],
+ "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC":
+ [""],
+ Account: [""],
+ "Bank host": [""],
+ "Bank transfer details": [""],
+ Subject: [""],
+ "Receiver name": [""],
+ "Could not load the transaction information": [""],
+ "There was an error trying to complete the transaction": [""],
+ "This transaction is not completed": [""],
+ Send: [""],
+ Retry: [""],
+ Forget: [""],
+ "Caution!": [""],
+ "If you have already wired money to the exchange you will loose the chance to get the coins form it.":
+ [""],
+ Confirm: [""],
+ Withdrawal: [""],
+ "Make sure to use the correct subject, otherwise the money will not arrive in this wallet.":
+ [""],
+ "The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check there is no pending step.":
+ [""],
+ "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins":
+ [""],
+ Details: [""],
+ Payment: [""],
+ Refunds: [""],
+ "%1$s %2$s on %3$s": [""],
+ "Merchant created a refund for this order but was not automatically picked up.":
+ [""],
+ Offer: [""],
+ Accept: [""],
+ Merchant: [""],
+ "Invoice ID": [""],
+ Deposit: [""],
+ Refresh: [""],
+ Tip: [""],
+ Refund: [""],
+ "Original order ID": [""],
+ "Purchase summary": [""],
+ copy: [""],
+ "hide qr": [""],
+ "show qr": [""],
+ Credit: [""],
+ Invoice: [""],
+ Exchange: [""],
+ URI: [""],
+ Debit: [""],
+ Transfer: [""],
+ Country: [""],
+ "Address lines": [""],
+ "Building number": [""],
+ "Building name": [""],
+ Street: [""],
+ "Post code": [""],
+ "Town location": [""],
+ Town: [""],
+ District: [""],
+ "Country subdivision": [""],
+ Date: [""],
+ "Transaction fees": [""],
+ Total: [""],
+ Withdraw: ["撤退"],
+ Price: [""],
+ Refunded: [""],
+ Delivery: [""],
+ "Total transfer": [""],
+ "Could not load pay status": [""],
+ "Digital cash payment": [""],
+ Purchase: [""],
+ Receipt: [""],
+ "Valid until": [""],
+ "List of products": [""],
+ free: [""],
+ "Already paid, you are going to be redirected to %1$s": [""],
+ "Already paid": [""],
+ "Already claimed": [""],
+ "Pay with a mobile phone": [""],
+ "Hide QR": [""],
+ "Scan the QR code or &nbsp; %1$s": [""],
+ "Pay &nbsp; %1$s": [""],
+ "You have no balance for this currency. Withdraw digital cash first.": [
"",
],
- "Accept fees and withdraw": [""],
- "Cancel withdraw operation": [""],
- "Withdrawal fees:": [""],
- "Rounding loss:": [""],
- "Earliest expiration (for deposit): %1$s": [""],
- "# Coins": [""],
- Value: [""],
- "Withdraw Fee": [""],
- "Refresh Fee": [""],
- "Deposit Fee": [""],
+ "Could not find enough coins to pay. Even if you have enough %1$s some restriction may apply.":
+ [""],
+ "Your current balance is not enough.": ["表示するバランスがありません"],
+ "Merchant message": [""],
+ "Could not load refund status": [""],
+ "Digital cash refund": [""],
+ "You&apos;ve ignored the tip.": [""],
+ "The refund is in progress.": [""],
+ "Total to refund": [""],
+ "The merchant &quot;%1$s&quot; is offering you a refund.": [""],
+ "Order amount": [""],
+ "Already refunded": [""],
+ "Refund offered": [""],
+ "Accept &nbsp; %1$s": [""],
+ "Could not load tip status": [""],
+ "Digital cash tip": [""],
+ "The merchant is offering you a tip": [""],
+ "Merchant URL": [""],
+ "Receive &nbsp; %1$s": [""],
+ "Tip from %1$s accepted. Check your transactions list for more details.":
+ [""],
+ "Select one option": [""],
+ "Could not load": [""],
+ "Show terms of service": [""],
+ "I accept the exchange terms of service": [""],
+ "Exchange doesn&apos;t have terms of service": [""],
+ "Review exchange terms of service": [""],
+ "Review new version of terms of service": [""],
+ "The exchange reply with a empty terms of service": [""],
+ "Download Terms of Service": [""],
+ "Hide terms of service": [""],
+ "Could not load exchange fees": [""],
+ Close: [""],
+ "could not find any exchange": [""],
+ "could not find any exchange for the currency %1$s": [""],
+ "Service fee description": [""],
+ "Select %1$s exchange": [""],
+ Reset: [""],
+ "Use this exchange": [""],
+ "Doesn&apos;t have auditors": [""],
+ currency: [""],
+ Operations: [""],
+ Deposits: [""],
+ Denomination: [""],
+ Until: [""],
+ Withdrawals: ["撤退"],
+ Currency: [""],
+ "Coin operations": [""],
+ "Every operation in this section may be different by denomination value and is valid for a period of time. The exchange will charge the indicated amount every time a coin is used in such operation.":
+ [""],
+ "Transfer operations": [""],
+ "Every operation in this section may be different by transfer type and is valid for a period of time. The exchange will charge the indicated amount every time a transfer is made.":
+ [""],
+ Operation: [""],
+ "Wallet operations": [""],
+ Feature: [""],
+ "Could not get the info from the URI": [""],
+ "Could not get info of withdrawal": [""],
+ "Digital cash withdrawal": [""],
+ "Could not finish the withdrawal operation": [""],
+ "Age restriction": [""],
+ "Withdraw &nbsp; %1$s": [""],
+ "Withdraw to a mobile phone": [""],
+ "Digital invoice": [""],
+ "Could not finish the invoice creation": [""],
+ Create: [""],
+ "Could not finish the payment operation": [""],
+ "Digital cash transfer": [""],
+ "Could not finish the transfer creation": [""],
+ "Could not finish the pickup operation": [""],
+ "Manual Withdrawal for %1$s": [""],
+ "Choose a exchange from where the coins will be withdrawn. The exchange will send the coins to this wallet after receiving a wire transfer with the correct subject.":
+ [""],
+ "No exchange found for %1$s": [""],
+ "Add Exchange": [""],
+ "No exchange configured": [""],
+ "Can&apos;t create the reserve": [""],
+ "Start withdrawal": [""],
+ "Could not load deposit balance": [""],
+ "A currency or an amount should be indicated": [""],
+ "There is no enough balance to make a deposit for currency %1$s": [""],
+ "Send %1$s to your account": [""],
+ "There is no account to make a deposit for currency %1$s": [""],
+ "Add account": [""],
+ "Select account": [""],
+ "Add another account": [""],
+ "Deposit fee": [""],
+ "Total deposit": [""],
+ "Deposit&nbsp;%1$s %2$s": [""],
+ "Add bank account for %1$s": [""],
+ "Enter the URL of an exchange you trust.": [""],
+ "Unable add this account": [""],
+ "Select account type": [""],
+ "Review terms of service": [""],
+ "Exchange URL": [""],
+ "Add exchange": [""],
+ "Add new exchange": [""],
+ "Add exchange for %1$s": [""],
+ "An exchange has been found! Review the information and click next": [""],
+ "This exchange doesn&apos;t match the expected currency %1$s": [""],
+ "Unable to verify this exchange": [""],
+ "Unable to add this exchange": [""],
+ loading: [""],
+ Version: [""],
+ Next: [""],
+ "Waiting for confirmation": [""],
+ PENDING: [""],
+ "Could not load the list of transactions": [""],
+ "Your transaction history is empty for this currency.": [""],
+ "Add backup provider": [""],
+ "Could not get provider information": [""],
+ "Backup providers may charge for their service": [""],
+ URL: [""],
+ Name: [""],
+ "Provider URL": [""],
+ "Please review and accept this provider&apos;s terms of service": [""],
+ Pricing: [""],
+ "free of charge": [""],
+ "%1$s per year of service": [""],
+ Storage: [""],
+ "%1$s megabytes of storage per year of service": [""],
+ "Accept terms of service": [""],
+ "Could not parse the payto URI": [""],
+ "Please check the uri": [""],
+ "Exchange is ready for withdrawal": [""],
+ "To complete the process you need to wire%1$s %2$s to the exchange bank account":
+ [""],
+ "Alternative, you can also scan this QR code or open %1$s if you have a banking app installed that supports RFC 8905":
+ [""],
+ "Cancel withdrawal": [""],
+ "Could not toggle auto-open": [""],
+ "Could not toggle clipboard": [""],
+ Navigator: [""],
+ "Automatically open wallet based on page content": [""],
+ "Enabling this option below will make using the wallet faster, but requires more permissions from your browser.":
+ [""],
+ "Automatically check clipboard for Taler URI": [""],
+ Trust: [""],
+ "No exchange yet": [""],
+ "Term of Service": [""],
+ ok: [""],
+ changed: [""],
+ "not accepted": [""],
+ "unknown (exchange status should be updated)": [""],
+ "Add an exchange": [""],
+ Troubleshooting: [""],
+ "Developer mode": [""],
+ "More options and information useful for debugging": [""],
+ Display: [""],
+ "Current Language": [""],
+ "Wallet Core": [""],
+ "Web Extension": [""],
+ "Exchange compatibility": [""],
+ "Merchant compatibility": [""],
+ "Bank compatibility": [""],
+ "Browser Extension Installed!": [""],
+ "You can open the GNU Taler Wallet using the combination %1$s .": [""],
+ "Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick access without keyboard:":
+ [""],
+ "Click the puzzle icon": [""],
+ "Search for GNU Taler Wallet": [""],
+ "Click the pin icon": [""],
+ Permissions: [""],
+ "(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)":
+ [""],
+ "Next Steps": [""],
+ "Try the demo": [""],
+ "Learn how to top up your wallet balance": [""],
+ "Diagnostics timed out. Could not talk to the wallet backend.": [""],
+ "Problems detected:": [""],
+ "Please check in your %1$s settings that you have IndexedDB enabled (check the preference name %2$s).":
+ [""],
+ "Your wallet database is outdated. Currently automatic migration is not supported. Please go %1$s to reset the wallet database.":
+ [""],
+ "Running diagnostics": [""],
+ "Debug tools": [""],
+ "Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?":
+ [""],
+ reset: [""],
+ "TESTING: This may delete all your coin, proceed with caution": [""],
+ "run gc": [""],
+ "import database": [""],
+ "export database": [""],
+ "Database exported at %1$s %2$s to download": [""],
+ Coins: [""],
+ "Pending operations": [""],
+ "usable coins": [""],
+ id: [""],
+ denom: [""],
+ value: [""],
+ status: [""],
+ "from refresh?": [""],
+ "age key count": [""],
+ "spent coins": [""],
+ "click to show": [""],
+ "Scan a QR code or enter taler:// URI below": [""],
+ Open: [""],
+ "URI is not valid. Taler URI should start with `taler://`": [""],
+ "Try another": [""],
+ "Could not load list of exchange": [""],
+ "Choose a currency to proceed or add another exchange": [""],
+ "Known currencies": [""],
+ "Specify the amount and the origin": [""],
+ "Change currency": [""],
+ "Use previous origins:": [""],
+ "Or specify the origin of the money": [""],
+ "Specify the origin of the money": [""],
+ "From my bank account": [""],
+ "From another wallet": [""],
+ "currency not provided": [""],
+ "Specify the amount and the destination": [""],
+ "Use previous destinations:": [""],
+ "Or specify the destination of the money": [""],
+ "Specify the destination of the money": [""],
+ "To my bank account": [""],
+ "To another wallet": [""],
+ "Could not load backup recovery information": [""],
+ "Digital wallet recovery": [""],
+ "Import backup, show info": [""],
+ "All done, your transaction is in progress": [""],
+ Edit: [""],
+ "Could not load the list of known exchanges": [""],
},
},
};
-strings["it"] = {
+strings["sv"] = {
domain: "messages",
locale_data: {
messages: {
"": {
domain: "messages",
- plural_forms: "nplurals=2; plural=(n != 1);",
- lang: "",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "sv",
},
- "Invalid Wire": [""],
- "Invalid Test Wire Detail": [""],
- "Test Wire Acct #%1$s on %2$s": [""],
- "Unknown Wire Detail": [""],
- Operation: [""],
- "time (ms/op)": [""],
- "The merchant %1$s offers you to purchase:": [""],
- "The total price is %1$s (plus %2$s fees).": [""],
- "The total price is %1$s.": [""],
- Retry: [""],
- "Confirm payment": [""],
- Balance: [""],
- History: [""],
- Debug: [""],
- "You have no balance to show. Need some %1$s getting started?": [""],
- "%1$s incoming": [""],
- "%1$s being spent": [""],
- "Error: could not retrieve balance information.": [""],
- "Invalid ": [""],
- "Fees ": [""],
- "Refresh sessions has completed": [""],
- "Order Refused": [""],
- "Order redirected": [""],
- "Payment aborted": [""],
- "Payment Sent": [""],
- "Order accepted": [""],
- "Reserve balance updated": [""],
- "Payment refund": [""],
- Withdrawn: [""],
- "Tip Accepted": [""],
- "Tip Declined": [""],
+ Balance: ["Balans"],
+ Backup: [""],
+ "QR Reader and Taler URI": [""],
+ Settings: [""],
+ Dev: [""],
"%1$s": [""],
- "Your wallet has no events recorded.": [""],
- "Wire to bank account": [""],
- Confirm: [""],
- Cancel: [""],
- "Could not get details for withdraw operation:": [""],
- "Chose different exchange provider": [""],
- "Please select an exchange. You can review the details before after your selection.": [
+ "PENDING OPERATIONS": [""],
+ Loading: [""],
+ "Could not load backup providers": [""],
+ "No backup providers configured": [""],
+ "Add provider": [""],
+ "Sync all backups": [""],
+ "Sync now": [""],
+ "Last synced": [""],
+ "Not synced": [""],
+ "Expires in": [""],
+ "There was an error loading the provider detail for &quot; %1$s&quot;": [
"",
],
- "Select %1$s": [""],
- "Select custom exchange": [""],
- "You are about to withdraw %1$s from your bank account into your wallet.": [
+ "There is not known provider with url &quot;%1$s&quot;.": [""],
+ "See providers": [""],
+ "Last backup": [""],
+ "Back up": [""],
+ "Provider fee": [""],
+ "per year": [""],
+ Extend: [""],
+ "terms has changed, extending the service will imply accepting the new terms of service":
+ [""],
+ old: [""],
+ new: [""],
+ fee: [""],
+ storage: [""],
+ "Remove provider": [""],
+ "This provider has reported an error": [""],
+ "There is conflict with another backup from %1$s": [""],
+ "Backup is not readable": [""],
+ "Unknown backup problem: %1$s": [""],
+ "service paid": [""],
+ "Backup valid until": [""],
+ Cancel: ["Avbryt"],
+ "Open reserve page": [""],
+ "Open pay page": [""],
+ "Open refund page": [""],
+ "Open tip page": [""],
+ "Open withdraw page": ["Utbetalnings avgift"],
+ "Get digital cash": ["Utbetalnings avgifter:"],
+ "Could not load balance page": [""],
+ Add: [""],
+ "Send %1$s": ["Välj %1$s"],
+ "Taler Action": [""],
+ "This page has pay action.": [""],
+ "This page has a withdrawal action.": [""],
+ "This page has a tip action.": [""],
+ "This page has a notify reserve action.": [""],
+ Notify: [""],
+ "This page has a refund action.": [""],
+ "This page has a malformed taler uri.": [""],
+ Dismiss: [""],
+ "this popup is being closed and you are being redirected to %1$s": [""],
+ "Could not load purchase proposal details": [""],
+ "Order Id": [""],
+ Summary: [""],
+ Amount: [""],
+ "Merchant name": [""],
+ "Merchant jurisdiction": [""],
+ "Merchant address": [""],
+ "Merchant logo": [""],
+ "Merchant website": [""],
+ "Merchant email": [""],
+ "Merchant public key": [""],
+ "Delivery date": [""],
+ "Delivery location": [""],
+ Products: [""],
+ "Created at": [""],
+ "Refund deadline": [""],
+ "Auto refund": [""],
+ "Pay deadline": [""],
+ "Fulfillment URL": [""],
+ "Fulfillment message": [""],
+ "Max deposit fee": ["Depostitions avgift"],
+ "Max fee": [""],
+ "Minimum age": [""],
+ "Wire fee amortization": [""],
+ Auditors: [""],
+ Exchanges: ["Accepterade tjänsteleverantörer:"],
+ "Bank account": ["Övervisa till bank konto"],
+ "Bitcoin address": [""],
+ IBAN: [""],
+ "Could not load deposit status": [""],
+ "Digital cash deposit": [""],
+ Cost: [""],
+ Fee: [""],
+ "To be received": [""],
+ "Send &nbsp; %1$s": [""],
+ "Bitcoin transfer details": [""],
+ "The exchange need a transaction with 3 output, one output is the exchange account and the other two are segwit fake address for metadata with an minimum amount.":
+ [""],
+ "In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional recipient and copy addresses and amounts":
+ [""],
+ "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC":
+ [""],
+ Account: [""],
+ "Bank host": [""],
+ "Bank transfer details": [""],
+ Subject: [""],
+ "Receiver name": [""],
+ "Could not load the transaction information": [""],
+ "There was an error trying to complete the transaction": [""],
+ "This transaction is not completed": [""],
+ Send: [""],
+ Retry: [""],
+ Forget: [""],
+ "Caution!": [""],
+ "If you have already wired money to the exchange you will loose the chance to get the coins form it.":
+ [""],
+ Confirm: ["Bekräfta"],
+ Withdrawal: ["Utbetalnings avgift"],
+ "Make sure to use the correct subject, otherwise the money will not arrive in this wallet.":
+ [""],
+ "The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check there is no pending step.":
+ [""],
+ "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins":
+ [""],
+ Details: [""],
+ Payment: ["Godkän betalning"],
+ Refunds: [""],
+ "%1$s %2$s on %3$s": ["Säljaren %1$sgav en återbetalning på %2$s.\n"],
+ "Merchant created a refund for this order but was not automatically picked up.":
+ [""],
+ Offer: [""],
+ Accept: [""],
+ Merchant: [""],
+ "Invoice ID": [""],
+ Deposit: ["Depostitions avgift"],
+ Refresh: ["Återhämtnings avgift"],
+ Tip: [""],
+ Refund: [""],
+ "Original order ID": [""],
+ "Purchase summary": [""],
+ copy: [""],
+ "hide qr": [""],
+ "show qr": [""],
+ Credit: [""],
+ Invoice: [""],
+ Exchange: [""],
+ URI: [""],
+ Debit: [""],
+ Transfer: [""],
+ Country: [""],
+ "Address lines": [""],
+ "Building number": [""],
+ "Building name": [""],
+ Street: [""],
+ "Post code": [""],
+ "Town location": [""],
+ Town: [""],
+ District: [""],
+ "Country subdivision": [""],
+ Date: [""],
+ "Transaction fees": [""],
+ Total: [""],
+ Withdraw: ["Utbetalnings avgift"],
+ Price: [""],
+ Refunded: [""],
+ Delivery: [""],
+ "Total transfer": ["Utbetalnings avgift"],
+ "Could not load pay status": [""],
+ "Digital cash payment": [""],
+ Purchase: [""],
+ Receipt: [""],
+ "Valid until": [""],
+ "List of products": [""],
+ free: [""],
+ "Already paid, you are going to be redirected to %1$s": [""],
+ "Already paid": [""],
+ "Already claimed": [""],
+ "Pay with a mobile phone": [""],
+ "Hide QR": [""],
+ "Scan the QR code or &nbsp; %1$s": [""],
+ "Pay &nbsp; %1$s": [""],
+ "You have no balance for this currency. Withdraw digital cash first.": [
"",
],
- "Accept fees and withdraw": [""],
- "Cancel withdraw operation": [""],
- "Withdrawal fees:": [""],
- "Rounding loss:": [""],
- "Earliest expiration (for deposit): %1$s": [""],
- "# Coins": [""],
- Value: [""],
- "Withdraw Fee": [""],
- "Refresh Fee": [""],
- "Deposit Fee": [""],
+ "Could not find enough coins to pay. Even if you have enough %1$s some restriction may apply.":
+ [""],
+ "Your current balance is not enough.": [
+ "Du har ingen balans att visa. Behöver du\n %1$s att börja?\n",
+ ],
+ "Merchant message": [""],
+ "Could not load refund status": [""],
+ "Digital cash refund": [""],
+ "You&apos;ve ignored the tip.": [""],
+ "The refund is in progress.": [""],
+ "Total to refund": ["Utbetalnings avgift"],
+ "The merchant &quot;%1$s&quot; is offering you a refund.": [
+ "Säljaren %1$s erbjuder följande:",
+ ],
+ "Order amount": ["Återhämtnings avgift"],
+ "Already refunded": [""],
+ "Refund offered": [""],
+ "Accept &nbsp; %1$s": [""],
+ "Could not load tip status": [""],
+ "Digital cash tip": [""],
+ "The merchant is offering you a tip": [
+ "Säljaren %1$s erbjuder följande:",
+ ],
+ "Merchant URL": [""],
+ "Receive &nbsp; %1$s": [""],
+ "Tip from %1$s accepted. Check your transactions list for more details.":
+ [""],
+ "Select one option": [""],
+ "Could not load": [""],
+ "Show terms of service": [""],
+ "I accept the exchange terms of service": [""],
+ "Exchange doesn&apos;t have terms of service": [""],
+ "Review exchange terms of service": [""],
+ "Review new version of terms of service": [""],
+ "The exchange reply with a empty terms of service": [""],
+ "Download Terms of Service": [""],
+ "Hide terms of service": [""],
+ "Could not load exchange fees": [""],
+ Close: [""],
+ "could not find any exchange": ["Accepterade tjänsteleverantörer:"],
+ "could not find any exchange for the currency %1$s": [""],
+ "Service fee description": [""],
+ "Select %1$s exchange": ["Välj %1$s"],
+ Reset: [""],
+ "Use this exchange": ["Accepterade tjänsteleverantörer:"],
+ "Doesn&apos;t have auditors": [""],
+ currency: [""],
+ Operations: [""],
+ Deposits: ["Depostitions avgift"],
+ Denomination: [""],
+ Until: [""],
+ Withdrawals: ["Utbetalnings avgift"],
+ Currency: [""],
+ "Coin operations": [""],
+ "Every operation in this section may be different by denomination value and is valid for a period of time. The exchange will charge the indicated amount every time a coin is used in such operation.":
+ [""],
+ "Transfer operations": [""],
+ "Every operation in this section may be different by transfer type and is valid for a period of time. The exchange will charge the indicated amount every time a transfer is made.":
+ [""],
+ Operation: [""],
+ "Wallet operations": [""],
+ Feature: [""],
+ "Could not get the info from the URI": [""],
+ "Could not get info of withdrawal": [""],
+ "Digital cash withdrawal": [""],
+ "Could not finish the withdrawal operation": [""],
+ "Age restriction": [""],
+ "Withdraw &nbsp; %1$s": [""],
+ "Withdraw to a mobile phone": [""],
+ "Digital invoice": [""],
+ "Could not finish the invoice creation": [""],
+ Create: [""],
+ "Could not finish the payment operation": [""],
+ "Digital cash transfer": [""],
+ "Could not finish the transfer creation": [""],
+ "Could not finish the pickup operation": [""],
+ "Manual Withdrawal for %1$s": ["Utbetalnings avgift"],
+ "Choose a exchange from where the coins will be withdrawn. The exchange will send the coins to this wallet after receiving a wire transfer with the correct subject.":
+ [""],
+ "No exchange found for %1$s": ["Accepterade tjänsteleverantörer:"],
+ "Add Exchange": ["Accepterade tjänsteleverantörer:"],
+ "No exchange configured": [""],
+ "Can&apos;t create the reserve": [""],
+ "Start withdrawal": [""],
+ "Could not load deposit balance": [""],
+ "A currency or an amount should be indicated": [""],
+ "There is no enough balance to make a deposit for currency %1$s": [""],
+ "Send %1$s to your account": [""],
+ "There is no account to make a deposit for currency %1$s": [""],
+ "Add account": ["Övervisa till bank konto"],
+ "Select account": ["Övervisa till bank konto"],
+ "Add another account": ["Övervisa till bank konto"],
+ "Deposit fee": ["Depostitions avgift"],
+ "Total deposit": [""],
+ "Deposit&nbsp;%1$s %2$s": ["Depostitions avgift"],
+ "Add bank account for %1$s": ["Accepterade tjänsteleverantörer:"],
+ "Enter the URL of an exchange you trust.": [""],
+ "Unable add this account": [""],
+ "Select account type": [""],
+ "Review terms of service": [""],
+ "Exchange URL": [""],
+ "Add exchange": ["Accepterade tjänsteleverantörer:"],
+ "Add new exchange": ["Accepterade tjänsteleverantörer:"],
+ "Add exchange for %1$s": ["Accepterade tjänsteleverantörer:"],
+ "An exchange has been found! Review the information and click next": [""],
+ "This exchange doesn&apos;t match the expected currency %1$s": [""],
+ "Unable to verify this exchange": [""],
+ "Unable to add this exchange": [""],
+ loading: [""],
+ Version: [""],
+ Next: [""],
+ "Waiting for confirmation": [""],
+ PENDING: [""],
+ "Could not load the list of transactions": [""],
+ "Your transaction history is empty for this currency.": [""],
+ "Add backup provider": [""],
+ "Could not get provider information": [""],
+ "Backup providers may charge for their service": [""],
+ URL: [""],
+ Name: [""],
+ "Provider URL": [""],
+ "Please review and accept this provider&apos;s terms of service": [""],
+ Pricing: [""],
+ "free of charge": [""],
+ "%1$s per year of service": [""],
+ Storage: [""],
+ "%1$s megabytes of storage per year of service": [""],
+ "Accept terms of service": [""],
+ "Could not parse the payto URI": [""],
+ "Please check the uri": [""],
+ "Exchange is ready for withdrawal": ["Tjänsteleverantörer i plånboken:"],
+ "To complete the process you need to wire%1$s %2$s to the exchange bank account":
+ [""],
+ "Alternative, you can also scan this QR code or open %1$s if you have a banking app installed that supports RFC 8905":
+ [""],
+ "Cancel withdrawal": [""],
+ "Could not toggle auto-open": [""],
+ "Could not toggle clipboard": [""],
+ Navigator: [""],
+ "Automatically open wallet based on page content": [""],
+ "Enabling this option below will make using the wallet faster, but requires more permissions from your browser.":
+ [""],
+ "Automatically check clipboard for Taler URI": [""],
+ Trust: [""],
+ "No exchange yet": [""],
+ "Term of Service": [""],
+ ok: [""],
+ changed: [""],
+ "not accepted": [""],
+ "unknown (exchange status should be updated)": [""],
+ "Add an exchange": ["Accepterade tjänsteleverantörer:"],
+ Troubleshooting: [""],
+ "Developer mode": [""],
+ "More options and information useful for debugging": [""],
+ Display: [""],
+ "Current Language": [""],
+ "Wallet Core": [""],
+ "Web Extension": [""],
+ "Exchange compatibility": [""],
+ "Merchant compatibility": [""],
+ "Bank compatibility": [""],
+ "Browser Extension Installed!": [""],
+ "You can open the GNU Taler Wallet using the combination %1$s .": [""],
+ "Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick access without keyboard:":
+ [""],
+ "Click the puzzle icon": [""],
+ "Search for GNU Taler Wallet": [""],
+ "Click the pin icon": [""],
+ Permissions: [""],
+ "(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)":
+ [""],
+ "Next Steps": [""],
+ "Try the demo": [""],
+ "Learn how to top up your wallet balance": [""],
+ "Diagnostics timed out. Could not talk to the wallet backend.": [""],
+ "Problems detected:": [""],
+ "Please check in your %1$s settings that you have IndexedDB enabled (check the preference name %2$s).":
+ [""],
+ "Your wallet database is outdated. Currently automatic migration is not supported. Please go %1$s to reset the wallet database.":
+ [""],
+ "Running diagnostics": [""],
+ "Debug tools": [""],
+ "Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?":
+ [""],
+ reset: [""],
+ "TESTING: This may delete all your coin, proceed with caution": [""],
+ "run gc": [""],
+ "import database": [""],
+ "export database": [""],
+ "Database exported at %1$s %2$s to download": [""],
+ Coins: ["# Mynt"],
+ "Pending operations": [""],
+ "usable coins": [""],
+ id: [""],
+ denom: [""],
+ value: ["Värde"],
+ status: [""],
+ "from refresh?": [""],
+ "age key count": ["Övervisa till bank konto"],
+ "spent coins": [""],
+ "click to show": [""],
+ "Scan a QR code or enter taler:// URI below": [""],
+ Open: [""],
+ "URI is not valid. Taler URI should start with `taler://`": [""],
+ "Try another": [""],
+ "Could not load list of exchange": [""],
+ "Choose a currency to proceed or add another exchange": [""],
+ "Known currencies": [""],
+ "Specify the amount and the origin": [""],
+ "Change currency": [""],
+ "Use previous origins:": [""],
+ "Or specify the origin of the money": [""],
+ "Specify the origin of the money": [""],
+ "From my bank account": ["Övervisa till bank konto"],
+ "From another wallet": [""],
+ "currency not provided": [""],
+ "Specify the amount and the destination": [""],
+ "Use previous destinations:": [""],
+ "Or specify the destination of the money": [""],
+ "Specify the destination of the money": [""],
+ "To my bank account": ["Övervisa till bank konto"],
+ "To another wallet": [""],
+ "Could not load backup recovery information": [""],
+ "Digital wallet recovery": [""],
+ "Import backup, show info": [""],
+ "All done, your transaction is in progress": [""],
+ Edit: [""],
+ "Could not load the list of known exchanges": [""],
},
},
};
-strings["sv"] = {
+strings["tr"] = {
domain: "messages",
locale_data: {
messages: {
"": {
domain: "messages",
- plural_forms: "nplurals=2; plural=(n != 1);",
- lang: "",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "tr",
},
- "Invalid Wire": [""],
- "Invalid Test Wire Detail": [""],
- "Test Wire Acct #%1$s on %2$s": [""],
- "Unknown Wire Detail": ["visa mer"],
- Operation: [""],
- "time (ms/op)": [""],
- "The merchant %1$s offers you to purchase:": [
- "Säljaren %1$s erbjuder följande:",
+ Balance: ["Bakiye"],
+ Backup: ["Yedekle"],
+ "QR Reader and Taler URI": [""],
+ Settings: ["Ayarlar"],
+ Dev: ["Gelişim"],
+ "%1$s": ["%1$s"],
+ "PENDING OPERATIONS": [""],
+ Loading: ["Yükleniyor"],
+ "Could not load backup providers": [
+ "Yedekleme sağlayıcıları yüklenemedi",
],
- "The total price is %1$s (plus %2$s fees).": [
- "Det totala priset är %1$s (plus %2$s avgifter).\n",
+ "No backup providers configured": [
+ "Yapılandırılmış yedekleme sağlayıcısı yok",
],
- "The total price is %1$s.": ["Det totala priset är %1$s."],
- Retry: [""],
- "Confirm payment": ["Godkän betalning"],
- Balance: ["Balans"],
- History: ["Historia"],
- Debug: [""],
- "You have no balance to show. Need some %1$s getting started?": [
- "Du har ingen balans att visa. Behöver du\n %1$s att börja?\n",
+ "Add provider": ["Sağlayıcı ekle"],
+ "Sync all backups": ["Tüm yedeklemeleri senkronize et"],
+ "Sync now": ["Şimdi senkronize et"],
+ "Last synced": ["Son Senkronizasyon"],
+ "Not synced": ["Senkronize Edilmedi"],
+ "Expires in": ["İçinde sona eriyor"],
+ "There was an error loading the provider detail for &quot; %1$s&quot;": [
+ "",
],
- "%1$s incoming": ["%1$s inkommande"],
- "%1$s being spent": [""],
- "Error: could not retrieve balance information.": [""],
- "Invalid ": [""],
- "Fees ": [""],
- "Refresh sessions has completed": [""],
- "Order Refused": [""],
- "Order redirected": [""],
- "Payment aborted": [""],
- "Payment Sent": [""],
- "Order accepted": [""],
- "Reserve balance updated": [""],
- "Payment refund": [""],
- Withdrawn: ["Utbetalnings avgift"],
- "Tip Accepted": [""],
- "Tip Declined": [""],
- "%1$s": [""],
- "Your wallet has no events recorded.": ["plånboken"],
- "Wire to bank account": ["Övervisa till bank konto"],
- Confirm: ["Bekräfta"],
- Cancel: ["Avbryt"],
- "Could not get details for withdraw operation:": [""],
- "Chose different exchange provider": ["Ändra tjänsteleverantörer"],
- "Please select an exchange. You can review the details before after your selection.": [
+ "There is not known provider with url &quot;%1$s&quot;.": [""],
+ "See providers": ["Sağlayıcı ekle"],
+ "Last backup": [""],
+ "Back up": [""],
+ "Provider fee": [""],
+ "per year": [""],
+ Extend: [""],
+ "terms has changed, extending the service will imply accepting the new terms of service":
+ [""],
+ old: [""],
+ new: [""],
+ fee: [""],
+ storage: [""],
+ "Remove provider": [""],
+ "This provider has reported an error": [""],
+ "There is conflict with another backup from %1$s": [""],
+ "Backup is not readable": [""],
+ "Unknown backup problem: %1$s": [""],
+ "service paid": [""],
+ "Backup valid until": [""],
+ Cancel: ["Bakiye"],
+ "Open reserve page": ["Rezerv sayfasını açın"],
+ "Open pay page": ["Ödeme sayfasını açın"],
+ "Open refund page": ["Geri ödeme sayfasını açın"],
+ "Open tip page": ["İkramiye sayfasını açın"],
+ "Open withdraw page": ["Para çekme sayfasını açın"],
+ "Get digital cash": [""],
+ "Could not load balance page": ["Bakiye sayfası yüklenemedi"],
+ Add: [""],
+ "Send %1$s": ["%1$s seçin"],
+ "Taler Action": ["Taler Eylemi"],
+ "This page has pay action.": ["Bu sayfada ödeme eylemi var."],
+ "This page has a withdrawal action.": [
+ "Bu sayfada para çekme eylemi var.",
+ ],
+ "This page has a tip action.": ["Bu sayfada bir ikramiye eylemi var."],
+ "This page has a notify reserve action.": [
+ "Bu sayfada bir rezervasyon bildir eylemi var.",
+ ],
+ Notify: ["Bildirin"],
+ "This page has a refund action.": [
+ "Bu sayfada bir geri ödeme eylemi var.",
+ ],
+ "This page has a malformed taler uri.": [
+ "Bu sayfada hatalı biçimlendirilmiş taler uri var.",
+ ],
+ Dismiss: ["Reddet"],
+ "this popup is being closed and you are being redirected to %1$s": [""],
+ "Could not load purchase proposal details": [
+ "Yedekleme sağlayıcıları yüklenemedi",
+ ],
+ "Order Id": ["Sipariş reddedildi"],
+ Summary: [""],
+ Amount: [""],
+ "Merchant name": [""],
+ "Merchant jurisdiction": [""],
+ "Merchant address": [""],
+ "Merchant logo": [""],
+ "Merchant website": [""],
+ "Merchant email": [""],
+ "Merchant public key": [""],
+ "Delivery date": [""],
+ "Delivery location": ["Taler Eylemi"],
+ Products: [""],
+ "Created at": [""],
+ "Refund deadline": [""],
+ "Auto refund": ["Ödeme iadesi"],
+ "Pay deadline": [""],
+ "Fulfillment URL": [""],
+ "Fulfillment message": [""],
+ "Max deposit fee": [""],
+ "Max fee": [""],
+ "Minimum age": [""],
+ "Wire fee amortization": [""],
+ Auditors: [""],
+ Exchanges: ["Exchange"],
+ "Bank account": [""],
+ "Bitcoin address": [""],
+ IBAN: [""],
+ "Could not load deposit status": ["Bakiye sayfası yüklenemedi"],
+ "Digital cash deposit": [""],
+ Cost: [""],
+ Fee: ["Ücretler"],
+ "To be received": [""],
+ "Send &nbsp; %1$s": [""],
+ "Bitcoin transfer details": [""],
+ "The exchange need a transaction with 3 output, one output is the exchange account and the other two are segwit fake address for metadata with an minimum amount.":
+ [""],
+ "In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional recipient and copy addresses and amounts":
+ [""],
+ "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC":
+ [""],
+ Account: [""],
+ "Bank host": [""],
+ "Bank transfer details": [""],
+ Subject: [""],
+ "Receiver name": [""],
+ "Could not load the transaction information": [""],
+ "There was an error trying to complete the transaction": [""],
+ "This transaction is not completed": [""],
+ Send: [""],
+ Retry: ["Yeniden deneyin"],
+ Forget: [""],
+ "Caution!": [""],
+ "If you have already wired money to the exchange you will loose the chance to get the coins form it.":
+ [""],
+ Confirm: ["Onaylamak"],
+ Withdrawal: ["Çekildi"],
+ "Make sure to use the correct subject, otherwise the money will not arrive in this wallet.":
+ [""],
+ "The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check there is no pending step.":
+ [""],
+ "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins":
+ [""],
+ Details: [""],
+ Payment: ["Ödeme gönderildi"],
+ Refunds: [""],
+ "%1$s %2$s on %3$s": [""],
+ "Merchant created a refund for this order but was not automatically picked up.":
+ [""],
+ Offer: [""],
+ Accept: ["İkramiye kabul edildi"],
+ Merchant: [""],
+ "Invoice ID": [""],
+ Deposit: [""],
+ Refresh: ["Ücreti yenile"],
+ Tip: [""],
+ Refund: [""],
+ "Original order ID": [""],
+ "Purchase summary": [""],
+ copy: [""],
+ "hide qr": [""],
+ "show qr": [""],
+ Credit: [""],
+ Invoice: [""],
+ Exchange: ["Exchange"],
+ URI: [""],
+ Debit: [""],
+ Transfer: [""],
+ Country: [""],
+ "Address lines": [""],
+ "Building number": [""],
+ "Building name": [""],
+ Street: [""],
+ "Post code": [""],
+ "Town location": [""],
+ Town: [""],
+ District: [""],
+ "Country subdivision": [""],
+ Date: [""],
+ "Transaction fees": [""],
+ Total: [""],
+ Withdraw: ["Para çek"],
+ Price: [""],
+ Refunded: [""],
+ Delivery: [""],
+ "Total transfer": ["Çekildi"],
+ "Could not load pay status": [""],
+ "Digital cash payment": [""],
+ Purchase: [""],
+ Receipt: [""],
+ "Valid until": [""],
+ "List of products": [""],
+ free: [""],
+ "Already paid, you are going to be redirected to %1$s": [""],
+ "Already paid": [""],
+ "Already claimed": [""],
+ "Pay with a mobile phone": [""],
+ "Hide QR": [""],
+ "Scan the QR code or &nbsp; %1$s": [""],
+ "Pay &nbsp; %1$s": [""],
+ "You have no balance for this currency. Withdraw digital cash first.": [
"",
],
- "Select %1$s": ["Välj %1$s"],
- "Select custom exchange": [""],
- "You are about to withdraw %1$s from your bank account into your wallet.": [
- "Du är på väg att ta ut\n %1$s från ditt bankkonto till din plånbok.\n",
- ],
- "Accept fees and withdraw": ["Acceptera avgifter och utbetala"],
- "Cancel withdraw operation": [""],
- "Withdrawal fees:": ["Utbetalnings avgifter:"],
- "Rounding loss:": [""],
- "Earliest expiration (for deposit): %1$s": [""],
- "# Coins": ["# Mynt"],
- Value: ["Värde"],
- "Withdraw Fee": ["Utbetalnings avgift"],
- "Refresh Fee": ["Återhämtnings avgift"],
- "Deposit Fee": ["Depostitions avgift"],
+ "Could not find enough coins to pay. Even if you have enough %1$s some restriction may apply.":
+ [""],
+ "Your current balance is not enough.": ["Gösterecek bakiyeniz yok."],
+ "Merchant message": [""],
+ "Could not load refund status": ["Bakiye sayfası yüklenemedi"],
+ "Digital cash refund": [""],
+ "You&apos;ve ignored the tip.": [""],
+ "The refund is in progress.": [""],
+ "Total to refund": ["Ödeme iadesi"],
+ "The merchant &quot;%1$s&quot; is offering you a refund.": [""],
+ "Order amount": ["Ücreti yenile"],
+ "Already refunded": ["Ödeme iadesi"],
+ "Refund offered": [""],
+ "Accept &nbsp; %1$s": [""],
+ "Could not load tip status": ["Bakiye sayfası yüklenemedi"],
+ "Digital cash tip": [""],
+ "The merchant is offering you a tip": [""],
+ "Merchant URL": [""],
+ "Receive &nbsp; %1$s": [""],
+ "Tip from %1$s accepted. Check your transactions list for more details.":
+ [""],
+ "Select one option": [""],
+ "Could not load": ["Bakiye sayfası yüklenemedi"],
+ "Show terms of service": ["Hizmet şartlarını göster"],
+ "I accept the exchange terms of service": [
+ "Hizmet şartlarını kabul ediyorum",
+ ],
+ "Exchange doesn&apos;t have terms of service": [
+ "Exchange'in hizmet şartları yok",
+ ],
+ "Review exchange terms of service": [
+ "Exchange'in hizmet şartlarını inceleyin",
+ ],
+ "Review new version of terms of service": [
+ "Hizmet şartlarının yeni sürümünü inceleyin",
+ ],
+ "The exchange reply with a empty terms of service": [""],
+ "Download Terms of Service": [""],
+ "Hide terms of service": [""],
+ "Could not load exchange fees": ["Bakiye sayfası yüklenemedi"],
+ Close: [""],
+ "could not find any exchange": ["Bakiye sayfası yüklenemedi"],
+ "could not find any exchange for the currency %1$s": [""],
+ "Service fee description": [""],
+ "Select %1$s exchange": ["Özel exchange'i seçin"],
+ Reset: [""],
+ "Use this exchange": ["Özel exchange'i seçin"],
+ "Doesn&apos;t have auditors": [""],
+ currency: [""],
+ Operations: ["Bekleyen işlemler"],
+ Deposits: ["Depozito %1$s"],
+ Denomination: ["Bekleyen işlemler"],
+ Until: [""],
+ Withdrawals: ["Çekildi"],
+ Currency: [""],
+ "Coin operations": ["Bekleyen işlemler"],
+ "Every operation in this section may be different by denomination value and is valid for a period of time. The exchange will charge the indicated amount every time a coin is used in such operation.":
+ [""],
+ "Transfer operations": ["Bekleyen işlemler"],
+ "Every operation in this section may be different by transfer type and is valid for a period of time. The exchange will charge the indicated amount every time a transfer is made.":
+ [""],
+ Operation: [""],
+ "Wallet operations": ["Bekleyen işlemler"],
+ Feature: [""],
+ "Could not get the info from the URI": [""],
+ "Could not get info of withdrawal": [
+ "Para çekme işlemi için ayrıntılar alınamadı:",
+ ],
+ "Digital cash withdrawal": [""],
+ "Could not finish the withdrawal operation": [""],
+ "Age restriction": [""],
+ "Withdraw &nbsp; %1$s": [""],
+ "Withdraw to a mobile phone": [""],
+ "Digital invoice": [""],
+ "Could not finish the invoice creation": [""],
+ Create: [""],
+ "Could not finish the payment operation": [
+ "Para çekme işlemi için ayrıntılar alınamadı:",
+ ],
+ "Digital cash transfer": [""],
+ "Could not finish the transfer creation": [
+ "Para çekme işlemi için ayrıntılar alınamadı:",
+ ],
+ "Could not finish the pickup operation": [
+ "Para çekme işlemi için ayrıntılar alınamadı:",
+ ],
+ "Manual Withdrawal for %1$s": ["Para çekme ücretleri:"],
+ "Choose a exchange from where the coins will be withdrawn. The exchange will send the coins to this wallet after receiving a wire transfer with the correct subject.":
+ [""],
+ "No exchange found for %1$s": [""],
+ "Add Exchange": [""],
+ "No exchange configured": [""],
+ "Can&apos;t create the reserve": [""],
+ "Start withdrawal": [""],
+ "Could not load deposit balance": ["Bakiye sayfası yüklenemedi"],
+ "A currency or an amount should be indicated": [""],
+ "There is no enough balance to make a deposit for currency %1$s": [""],
+ "Send %1$s to your account": [""],
+ "There is no account to make a deposit for currency %1$s": [""],
+ "Add account": [""],
+ "Select account": [""],
+ "Add another account": [""],
+ "Deposit fee": [""],
+ "Total deposit": [""],
+ "Deposit&nbsp;%1$s %2$s": ["Test Havale Hesap #%1$s üzerinde %2$s"],
+ "Add bank account for %1$s": [""],
+ "Enter the URL of an exchange you trust.": [""],
+ "Unable add this account": [""],
+ "Select account type": [""],
+ "Review terms of service": [""],
+ "Exchange URL": [""],
+ "Add exchange": [""],
+ "Add new exchange": [""],
+ "Add exchange for %1$s": [""],
+ "An exchange has been found! Review the information and click next": [""],
+ "This exchange doesn&apos;t match the expected currency %1$s": [""],
+ "Unable to verify this exchange": [""],
+ "Unable to add this exchange": [""],
+ loading: [""],
+ Version: [""],
+ Next: [""],
+ "Waiting for confirmation": [""],
+ PENDING: [""],
+ "Could not load the list of transactions": [""],
+ "Your transaction history is empty for this currency.": [""],
+ "Add backup provider": [""],
+ "Could not get provider information": [""],
+ "Backup providers may charge for their service": [""],
+ URL: [""],
+ Name: [""],
+ "Provider URL": [""],
+ "Please review and accept this provider&apos;s terms of service": [
+ "Hizmet şartlarını kabul ediyorum",
+ ],
+ Pricing: [""],
+ "free of charge": [""],
+ "%1$s per year of service": [""],
+ Storage: [""],
+ "%1$s megabytes of storage per year of service": [""],
+ "Accept terms of service": [""],
+ "Could not parse the payto URI": [""],
+ "Please check the uri": [""],
+ "Exchange is ready for withdrawal": [""],
+ "To complete the process you need to wire%1$s %2$s to the exchange bank account":
+ [""],
+ "Alternative, you can also scan this QR code or open %1$s if you have a banking app installed that supports RFC 8905":
+ [""],
+ "Cancel withdrawal": [""],
+ "Could not toggle auto-open": ["Bakiye sayfası yüklenemedi"],
+ "Could not toggle clipboard": ["Bakiye sayfası yüklenemedi"],
+ Navigator: [""],
+ "Automatically open wallet based on page content": [""],
+ "Enabling this option below will make using the wallet faster, but requires more permissions from your browser.":
+ [""],
+ "Automatically check clipboard for Taler URI": [""],
+ Trust: [""],
+ "No exchange yet": [""],
+ "Term of Service": [""],
+ ok: [""],
+ changed: [""],
+ "not accepted": [""],
+ "unknown (exchange status should be updated)": [""],
+ "Add an exchange": [""],
+ Troubleshooting: [""],
+ "Developer mode": [""],
+ "More options and information useful for debugging": [""],
+ Display: [""],
+ "Current Language": [""],
+ "Wallet Core": [""],
+ "Web Extension": [""],
+ "Exchange compatibility": [""],
+ "Merchant compatibility": [""],
+ "Bank compatibility": [""],
+ "Browser Extension Installed!": [""],
+ "You can open the GNU Taler Wallet using the combination %1$s .": [""],
+ "Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick access without keyboard:":
+ [""],
+ "Click the puzzle icon": [""],
+ "Search for GNU Taler Wallet": [""],
+ "Click the pin icon": [""],
+ Permissions: [""],
+ "(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)":
+ [""],
+ "Next Steps": [""],
+ "Try the demo": [""],
+ "Learn how to top up your wallet balance": [""],
+ "Diagnostics timed out. Could not talk to the wallet backend.": [
+ "Tanılar zaman aşımına uğradı. Cüzdan arka ucuyla konuşulamadı.",
+ ],
+ "Problems detected:": ["Tespit edilen sorunlar:"],
+ "Please check in your %1$s settings that you have IndexedDB enabled (check the preference name %2$s).":
+ [
+ "Lütfen %1$s ayarlarınızda, IndexedDB'nin etkinleştirildiğinizi kontrol edin (%2$s tercih adını kontrol edin).",
+ ],
+ "Your wallet database is outdated. Currently automatic migration is not supported. Please go %1$s to reset the wallet database.":
+ [
+ "Cüzdan veritabanınız eski. Şu anda otomatik aktarım desteklenmiyor. Cüzdan veritabanını sıfırlamak için lütfen %1$s gidin.",
+ ],
+ "Running diagnostics": ["Tanılamayı çalıştır"],
+ "Debug tools": ["Hata ayıklama araçları"],
+ "Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?":
+ [
+ "Cüzdanınızdaki her şeyi GERİ ALINAMAZ BİÇİMDE İMHA ETMEK ve TÜM PARALARINIZI KAYBETMEK mi istiyorsunuz?",
+ ],
+ reset: ["sıfırla"],
+ "TESTING: This may delete all your coin, proceed with caution": [""],
+ "run gc": [""],
+ "import database": ["veritabanını içe aktar"],
+ "export database": ["veritabanını dışa aktar"],
+ "Database exported at %1$s %2$s to download": [
+ "Veritabanı %1$s'de dışa aktarıldı-%2$s indirilecek",
+ ],
+ Coins: ["Madeni paralar"],
+ "Pending operations": ["Bekleyen işlemler"],
+ "usable coins": ["kullanılabilir madeni paralar"],
+ id: ["kimlik"],
+ denom: [""],
+ value: ["değer"],
+ status: ["durum"],
+ "from refresh?": ["yenilemeden mi?"],
+ "age key count": [""],
+ "spent coins": ["harcanan madeni paralar"],
+ "click to show": ["göstermek için tıklayın"],
+ "Scan a QR code or enter taler:// URI below": [""],
+ Open: [""],
+ "URI is not valid. Taler URI should start with `taler://`": [""],
+ "Try another": [""],
+ "Could not load list of exchange": ["Bakiye sayfası yüklenemedi"],
+ "Choose a currency to proceed or add another exchange": [""],
+ "Known currencies": [""],
+ "Specify the amount and the origin": [""],
+ "Change currency": [""],
+ "Use previous origins:": [""],
+ "Or specify the origin of the money": [""],
+ "Specify the origin of the money": [""],
+ "From my bank account": ["Banka hesabına havale yap"],
+ "From another wallet": [""],
+ "currency not provided": [""],
+ "Specify the amount and the destination": [""],
+ "Use previous destinations:": [""],
+ "Or specify the destination of the money": [""],
+ "Specify the destination of the money": [""],
+ "To my bank account": ["Banka hesabına havale yap"],
+ "To another wallet": [""],
+ "Could not load backup recovery information": [
+ "Yedekleme sağlayıcıları yüklenemedi",
+ ],
+ "Digital wallet recovery": [""],
+ "Import backup, show info": [""],
+ "All done, your transaction is in progress": [""],
+ Edit: [
+ "Lütfen bir exchange seçin. Detayları seçiminizden önce inceleyebilirsiniz.",
+ ],
+ "Could not load the list of known exchanges": [""],
},
},
};
diff --git a/packages/taler-wallet-webextension/src/i18n/sv.po b/packages/taler-wallet-webextension/src/i18n/sv.po
index c6a739789..bb3caea4b 100644
--- a/packages/taler-wallet-webextension/src/i18n/sv.po
+++ b/packages/taler-wallet-webextension/src/i18n/sv.po
@@ -1,351 +1,2061 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
#
-# TALER is free software; you can redistribute it and/or modify it under the
+# GNU Taler is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3, or (at your option) any later version.
#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
-"Report-Msgid-Bugs-To: \n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: Flo Reitz <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"PO-Revision-Date: 2023-03-06 22:06+0000\n"
+"Last-Translator: Anonymous <noreply@weblate.org>\n"
+"Language-Team: Swedish <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/sv/>\n"
+"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Balans"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr ""
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr ""
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr ""
-#: src/util/wire.ts:37
+#: src/NavigationBar.tsx:184
#, c-format
-msgid "Invalid Wire"
+msgid "Dev"
+msgstr ""
+
+#: src/mui/Typography.tsx:122
+#, c-format
+msgid "%1$s"
msgstr ""
-#: src/util/wire.ts:42 src/util/wire.ts:45
+#: src/components/PendingTransactions.tsx:74
#, c-format
-msgid "Invalid Test Wire Detail"
+msgid "PENDING OPERATIONS"
msgstr ""
-#: src/util/wire.ts:47
+#: src/components/Loading.tsx:36
#, c-format
-msgid "Test Wire Acct #%1$s on %2$s"
+msgid "Loading"
msgstr ""
-#: src/util/wire.ts:49
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr ""
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms "
+"of service"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Avbryt"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:76
+#, fuzzy, c-format
+msgid "Open withdraw page"
+msgstr "Utbetalnings avgift"
+
+#: src/popup/NoBalanceHelp.tsx:43
#, fuzzy, c-format
-msgid "Unknown Wire Detail"
-msgstr "visa mer"
+msgid "Get digital cash"
+msgstr "Utbetalnings avgifter:"
-#: src/webex/pages/benchmark.tsx:52
+#: src/popup/BalancePage.tsx:138
#, c-format
-msgid "Operation"
+msgid "Could not load balance page"
msgstr ""
-#: src/webex/pages/benchmark.tsx:53
+#: src/popup/BalancePage.tsx:175
#, c-format
-msgid "time (ms/op)"
+msgid "Add"
msgstr ""
-#: src/webex/pages/pay.tsx:130
+#: src/popup/BalancePage.tsx:179
#, fuzzy, c-format
-msgid "The merchant %1$s offers you to purchase:"
-msgstr "Säljaren %1$s erbjuder följande:"
+msgid "Send %1$s"
+msgstr "Välj %1$s"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr ""
-#: src/webex/pages/pay.tsx:136
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, fuzzy, c-format
+msgid "Max deposit fee"
+msgstr "Depostitions avgift"
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
#, fuzzy, c-format
-msgid "The total price is %1$s (plus %2$s fees)."
-msgstr "Det totala priset är %1$s (plus %2$s avgifter).\n"
+msgid "Exchanges"
+msgstr "Accepterade tjänsteleverantörer:"
-#: src/webex/pages/pay.tsx:141
+#: src/components/Part.tsx:148
#, fuzzy, c-format
-msgid "The total price is %1$s."
-msgstr "Det totala priset är %1$s."
+msgid "Bank account"
+msgstr "Övervisa till bank konto"
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
-#: src/webex/pages/pay.tsx:163
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an "
+"minimum amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two "
+"additional recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid ""
+"Make sure the amount show %1$s BTC, else you have to change the base unit to "
+"BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
#, c-format
msgid "Retry"
msgstr ""
-#: src/webex/pages/pay.tsx:173
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to "
+"get the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr "Bekräfta"
+
+#: src/wallet/Transaction.tsx:267
+#, fuzzy, c-format
+msgid "Withdrawal"
+msgstr "Utbetalnings avgift"
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and "
+"check there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid ""
+"Bank has confirmed the wire transfer. Waiting for the exchange to send the "
+"coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
#, c-format
-msgid "Confirm payment"
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, fuzzy, c-format
+msgid "Payment"
msgstr "Godkän betalning"
-#: src/webex/pages/popup.tsx:153
+#: src/wallet/Transaction.tsx:378
#, c-format
-msgid "Balance"
-msgstr "Balans"
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, fuzzy, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr "Säljaren %1$sgav en återbetalning på %2$s.\n"
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid ""
+"Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, fuzzy, c-format
+msgid "Deposit"
+msgstr "Depostitions avgift"
+
+#: src/wallet/Transaction.tsx:496
+#, fuzzy, c-format
+msgid "Refresh"
+msgstr "Återhämtnings avgift"
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, fuzzy, c-format
+msgid "Withdraw"
+msgstr "Utbetalnings avgift"
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, fuzzy, c-format
+msgid "Total transfer"
+msgstr "Utbetalnings avgift"
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
-#: src/webex/pages/popup.tsx:154
+#: src/cta/Payment/views.tsx:280
#, c-format
-msgid "History"
-msgstr "Historia"
+msgid "Already claimed"
+msgstr ""
-#: src/webex/pages/popup.tsx:155
+#: src/cta/Payment/views.tsx:296
#, c-format
-msgid "Debug"
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
msgstr ""
-#: src/webex/pages/popup.tsx:175
+#: src/cta/Payment/views.tsx:366
#, fuzzy, c-format
-msgid "You have no balance to show. Need some %1$s getting started?"
+msgid "Your current balance is not enough."
msgstr ""
"Du har ingen balans att visa. Behöver du\n"
" %1$s att börja?\n"
-#: src/webex/pages/popup.tsx:238
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, fuzzy, c-format
+msgid "Total to refund"
+msgstr "Utbetalnings avgift"
+
+#: src/cta/Refund/views.tsx:106
+#, fuzzy, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr "Säljaren %1$s erbjuder följande:"
+
+#: src/cta/Refund/views.tsx:115
+#, fuzzy, c-format
+msgid "Order amount"
+msgstr "Återhämtnings avgift"
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, fuzzy, c-format
+msgid "The merchant is offering you a tip"
+msgstr "Säljaren %1$s erbjuder följande:"
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, fuzzy, c-format
+msgid "could not find any exchange"
+msgstr "Accepterade tjänsteleverantörer:"
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, fuzzy, c-format
+msgid "Select %1$s exchange"
+msgstr "Välj %1$s"
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, fuzzy, c-format
+msgid "Use this exchange"
+msgstr "Accepterade tjänsteleverantörer:"
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
#, fuzzy, c-format
-msgid "%1$s incoming"
-msgstr "%1$s inkommande"
+msgid "Deposits"
+msgstr "Depostitions avgift"
-#: src/webex/pages/popup.tsx:250
+#: src/wallet/ExchangeSelection/views.tsx:259
#, c-format
-msgid "%1$s being spent"
+msgid "Denomination"
msgstr ""
-#: src/webex/pages/popup.tsx:281
+#: src/wallet/ExchangeSelection/views.tsx:265
#, c-format
-msgid "Error: could not retrieve balance information."
+msgid "Until"
msgstr ""
-#: src/webex/pages/popup.tsx:390
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, fuzzy, c-format
+msgid "Withdrawals"
+msgstr "Utbetalnings avgift"
+
+#: src/wallet/ExchangeSelection/views.tsx:423
#, c-format
-msgid "Invalid "
+msgid "Currency"
msgstr ""
-#: src/webex/pages/popup.tsx:396
+#: src/wallet/ExchangeSelection/views.tsx:433
#, c-format
-msgid "Fees "
+msgid "Coin operations"
msgstr ""
-#: src/webex/pages/popup.tsx:434
+#: src/wallet/ExchangeSelection/views.tsx:436
#, c-format
-msgid "Refresh sessions has completed"
+msgid ""
+"Every operation in this section may be different by denomination value and "
+"is valid for a period of time. The exchange will charge the indicated amount "
+"every time a coin is used in such operation."
msgstr ""
-#: src/webex/pages/popup.tsx:451
+#: src/wallet/ExchangeSelection/views.tsx:545
#, c-format
-msgid "Order Refused"
+msgid "Transfer operations"
msgstr ""
-#: src/webex/pages/popup.tsx:465
+#: src/wallet/ExchangeSelection/views.tsx:548
#, c-format
-msgid "Order redirected"
+msgid ""
+"Every operation in this section may be different by transfer type and is "
+"valid for a period of time. The exchange will charge the indicated amount "
+"every time a transfer is made."
msgstr ""
-#: src/webex/pages/popup.tsx:482
+#: src/wallet/ExchangeSelection/views.tsx:563
#, c-format
-msgid "Payment aborted"
+msgid "Operation"
msgstr ""
-#: src/webex/pages/popup.tsx:512
+#: src/wallet/ExchangeSelection/views.tsx:583
#, c-format
-msgid "Payment Sent"
+msgid "Wallet operations"
msgstr ""
-#: src/webex/pages/popup.tsx:536
+#: src/wallet/ExchangeSelection/views.tsx:597
#, c-format
-msgid "Order accepted"
+msgid "Feature"
msgstr ""
-#: src/webex/pages/popup.tsx:547
+#: src/cta/Withdraw/views.tsx:47
#, c-format
-msgid "Reserve balance updated"
+msgid "Could not get the info from the URI"
msgstr ""
-#: src/webex/pages/popup.tsx:559
+#: src/cta/Withdraw/views.tsx:60
#, c-format
-msgid "Payment refund"
+msgid "Could not get info of withdrawal"
msgstr ""
-#: src/webex/pages/popup.tsx:584
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
#, fuzzy, c-format
-msgid "Withdrawn"
+msgid "Manual Withdrawal for %1$s"
msgstr "Utbetalnings avgift"
-#: src/webex/pages/popup.tsx:596
+#: src/wallet/CreateManualWithdraw.tsx:154
#, c-format
-msgid "Tip Accepted"
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will "
+"send the coins to this wallet after receiving a wire transfer with the "
+"correct subject."
msgstr ""
-#: src/webex/pages/popup.tsx:606
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, fuzzy, c-format
+msgid "No exchange found for %1$s"
+msgstr "Accepterade tjänsteleverantörer:"
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, fuzzy, c-format
+msgid "Add Exchange"
+msgstr "Accepterade tjänsteleverantörer:"
+
+#: src/wallet/CreateManualWithdraw.tsx:192
#, c-format
-msgid "Tip Declined"
+msgid "No exchange configured"
msgstr ""
-#: src/webex/pages/popup.tsx:615
+#: src/wallet/CreateManualWithdraw.tsx:210
#, c-format
-msgid "%1$s"
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
msgstr ""
-#: src/webex/pages/popup.tsx:707
+#: src/wallet/DepositPage/views.tsx:117
#, c-format
-msgid "Your wallet has no events recorded."
-msgstr "plånboken"
+msgid "Send %1$s to your account"
+msgstr ""
-#: src/webex/pages/return-coins.tsx:124
+#: src/wallet/DepositPage/views.tsx:121
#, c-format
-msgid "Wire to bank account"
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, fuzzy, c-format
+msgid "Add account"
+msgstr "Övervisa till bank konto"
+
+#: src/wallet/DepositPage/views.tsx:151
+#, fuzzy, c-format
+msgid "Select account"
msgstr "Övervisa till bank konto"
-#: src/webex/pages/return-coins.tsx:206
+#: src/wallet/DepositPage/views.tsx:163
+#, fuzzy, c-format
+msgid "Add another account"
+msgstr "Övervisa till bank konto"
+
+#: src/wallet/DepositPage/views.tsx:191
+#, fuzzy, c-format
+msgid "Deposit fee"
+msgstr "Depostitions avgift"
+
+#: src/wallet/DepositPage/views.tsx:205
#, c-format
-msgid "Confirm"
-msgstr "Bekräfta"
+msgid "Total deposit"
+msgstr ""
-#: src/webex/pages/return-coins.tsx:209
+#: src/wallet/DepositPage/views.tsx:233
+#, fuzzy, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr "Depostitions avgift"
+
+#: src/wallet/AddAccount/views.tsx:56
+#, fuzzy, c-format
+msgid "Add bank account for %1$s"
+msgstr "Accepterade tjänsteleverantörer:"
+
+#: src/wallet/AddAccount/views.tsx:59
#, c-format
-msgid "Cancel"
-msgstr "Avbryt"
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
-#: src/webex/pages/withdraw.tsx:73
+#: src/wallet/ExchangeAddConfirm.tsx:45
#, c-format
-msgid "Could not get details for withdraw operation:"
+msgid "Exchange URL"
msgstr ""
-#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, fuzzy, c-format
+msgid "Add exchange"
+msgstr "Accepterade tjänsteleverantörer:"
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, fuzzy, c-format
+msgid "Add new exchange"
+msgstr "Accepterade tjänsteleverantörer:"
+
+#: src/wallet/ExchangeSetUrl.tsx:116
#, fuzzy, c-format
-msgid "Chose different exchange provider"
-msgstr "Ändra tjänsteleverantörer"
+msgid "Add exchange for %1$s"
+msgstr "Accepterade tjänsteleverantörer:"
-#: src/webex/pages/withdraw.tsx:109
+#: src/wallet/ExchangeSetUrl.tsx:128
#, c-format
-msgid ""
-"Please select an exchange. You can review the details before after your "
-"selection."
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
msgstr ""
-#: src/webex/pages/withdraw.tsx:121
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
#, fuzzy, c-format
-msgid "Select %1$s"
-msgstr "Välj %1$s"
+msgid "Exchange is ready for withdrawal"
+msgstr "Tjänsteleverantörer i plånboken:"
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid ""
+"To complete the process you need to wire%1$s %2$s to the exchange bank "
+"account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a "
+"banking app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
-#: src/webex/pages/withdraw.tsx:143
+#: src/wallet/Settings.tsx:129
#, c-format
-msgid "Select custom exchange"
+msgid "Automatically open wallet based on page content"
msgstr ""
-#: src/webex/pages/withdraw.tsx:163
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
#, fuzzy, c-format
-msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgid "Add an exchange"
+msgstr "Accepterade tjänsteleverantörer:"
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
msgstr ""
-"Du är på väg att ta ut\n"
-" %1$s från ditt bankkonto till din plånbok.\n"
-#: src/webex/pages/withdraw.tsx:174
+#: src/wallet/Settings.tsx:244
#, c-format
-msgid "Accept fees and withdraw"
-msgstr "Acceptera avgifter och utbetala"
+msgid "Developer mode"
+msgstr ""
-#: src/webex/pages/withdraw.tsx:192
+#: src/wallet/Settings.tsx:246
#, c-format
-msgid "Cancel withdraw operation"
+msgid "More options and information useful for debugging"
msgstr ""
-#: src/webex/renderHtml.tsx:249
+#: src/wallet/Settings.tsx:257
#, c-format
-msgid "Withdrawal fees:"
-msgstr "Utbetalnings avgifter:"
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check "
+"the preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE "
+"ALL YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
-#: src/webex/renderHtml.tsx:252
+#: src/wallet/DeveloperPage.tsx:197
#, c-format
-msgid "Rounding loss:"
+msgid "import database"
msgstr ""
-#: src/webex/renderHtml.tsx:254
+#: src/wallet/DeveloperPage.tsx:219
#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
+msgid "export database"
msgstr ""
-#: src/webex/renderHtml.tsx:262
+#: src/wallet/DeveloperPage.tsx:225
#, c-format
-msgid "# Coins"
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, fuzzy, c-format
+msgid "Coins"
msgstr "# Mynt"
-#: src/webex/renderHtml.tsx:263
+#: src/wallet/DeveloperPage.tsx:282
#, c-format
-msgid "Value"
-msgstr "Värde"
+msgid "Pending operations"
+msgstr ""
-#: src/webex/renderHtml.tsx:264
+#: src/wallet/DeveloperPage.tsx:328
#, c-format
-msgid "Withdraw Fee"
-msgstr "Utbetalnings avgift"
+msgid "usable coins"
+msgstr ""
-#: src/webex/renderHtml.tsx:265
+#: src/wallet/DeveloperPage.tsx:337
#, c-format
-msgid "Refresh Fee"
-msgstr "Återhämtnings avgift"
+msgid "id"
+msgstr ""
-#: src/webex/renderHtml.tsx:266
+#: src/wallet/DeveloperPage.tsx:340
#, c-format
-msgid "Deposit Fee"
-msgstr "Depostitions avgift"
+msgid "denom"
+msgstr ""
+#: src/wallet/DeveloperPage.tsx:343
#, fuzzy, c-format
-#~ msgid "Merchant %1$s offered contract %2$s."
-#~ msgstr "Säljaren %1$s erbjöd kontrakt %2$s.\n"
+msgid "value"
+msgstr "Värde"
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
#, fuzzy, c-format
-#~ msgid "Merchant %1$s gave a refund over %2$s."
-#~ msgstr "Säljaren %1$sgav en återbetalning på %2$s.\n"
+msgid "age key count"
+msgstr "Övervisa till bank konto"
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
#, fuzzy, c-format
-#~ msgid "Merchant %1$s gave a %2$s of %3$s."
-#~ msgstr "Säljaren %1$sgav en återbetalning på %2$s.\n"
+msgid "From my bank account"
+msgstr "Övervisa till bank konto"
+#: src/wallet/DestinationSelection.tsx:395
#, c-format
-#~ msgid "help"
-#~ msgstr "hjälp"
+msgid "From another wallet"
+msgstr ""
+#: src/wallet/DestinationSelection.tsx:449
#, c-format
-#~ msgid "Payback"
-#~ msgstr "Återbetalning"
+msgid "currency not provided"
+msgstr ""
+#: src/wallet/DestinationSelection.tsx:459
#, c-format
-#~ msgid "Return Electronic Cash to Bank Account"
-#~ msgstr "Återlämna elektroniska pengar till bank konto"
+msgid "Specify the amount and the destination"
+msgstr ""
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
#, fuzzy, c-format
-#~ msgid "show more details"
-#~ msgstr "visa mer"
+msgid "To my bank account"
+msgstr "Övervisa till bank konto"
+#: src/wallet/DestinationSelection.tsx:534
#, c-format
-#~ msgid "Accepted exchanges:"
-#~ msgstr "Accepterade tjänsteleverantörer:"
+msgid "To another wallet"
+msgstr ""
+#: src/cta/Recovery/views.tsx:30
#, c-format
-#~ msgid "Exchanges in the wallet:"
-#~ msgstr "Tjänsteleverantörer i plånboken:"
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+#: src/wallet/ManualWithdrawPage.tsx:102
#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
+
+#, fuzzy
+#~ msgid "back"
+#~ msgstr "Återbetalning"
+
+#, fuzzy
+#~ msgid "no balance"
+#~ msgstr "Balans"
+
+#, fuzzy
+#~ msgid "Exchange fee"
+#~ msgstr "Tjänsteleverantörer i plånboken:"
+
+#, fuzzy
+#~ msgid "Deposit amount"
+#~ msgstr "Depostitions avgift"
+
+#, fuzzy
+#~ msgid "Withdraw anyway"
+#~ msgstr "Utbetalnings avgift"
+
+#, fuzzy
+#~ msgid "Unknown Wire Detail"
+#~ msgstr "visa mer"
+
+#~ msgid "The total price is %1$s (plus %2$s fees)."
+#~ msgstr "Det totala priset är %1$s (plus %2$s avgifter)."
+
+#, fuzzy
+#~ msgid "The total price is %1$s."
+#~ msgstr "Det totala priset är %1$s."
+
+#~ msgid "Confirm payment"
+#~ msgstr "Godkän betalning"
+
+#~ msgid "History"
+#~ msgstr "Historia"
+
+#, fuzzy
+#~ msgid "%1$s incoming"
+#~ msgstr "%1$s inkommande"
+
+#~ msgid "Your wallet has no events recorded."
+#~ msgstr "plånboken"
+
+#, fuzzy
+#~ msgid "Chose different exchange provider"
+#~ msgstr "Ändra tjänsteleverantörer"
+
+#~ msgid ""
+#~ "You are about to withdraw %1$s from your bank account into your wallet."
+#~ msgstr "Du är på väg att ta ut %1$s från ditt bankkonto till din plånbok."
+
+#~ msgid "Accept fees and withdraw"
+#~ msgstr "Acceptera avgifter och utbetala"
+
+#, fuzzy
+#~ msgid "Merchant %1$s offered contract %2$s."
+#~ msgstr "Säljaren %1$s erbjöd kontrakt %2$s.\n"
+
+#, fuzzy
+#~ msgid "Merchant %1$s gave a refund over %2$s."
+#~ msgstr "Säljaren %1$sgav en återbetalning på %2$s.\n"
+
+#~ msgid "help"
+#~ msgstr "hjälp"
+
+#~ msgid "Return Electronic Cash to Bank Account"
+#~ msgstr "Återlämna elektroniska pengar till bank konto"
+
+#, fuzzy
+#~ msgid "show more details"
+#~ msgstr "visa mer"
+
#~ msgid ""
#~ "You have insufficient funds of the requested currency in your wallet."
#~ msgstr "plånboken"
-#, c-format
#~ msgid ""
#~ "You do not have any funds from an exchange that is accepted by this "
#~ "merchant. None of the exchanges accepted by the merchant is known to your "
#~ "wallet."
#~ msgstr "plånboken"
-#, c-format
#~ msgid "Submitting payment"
#~ msgstr "Bekräftar betalning"
-#, c-format
#~ msgid ""
#~ "You already paid for this, clicking \"Confirm payment\" will not cost "
#~ "money again."
@@ -353,36 +2063,27 @@ msgstr "Depostitions avgift"
#~ "Du har redan betalat för det här, om du trycker \"Godkän betalning\" "
#~ "debiteras du inte igen"
-#, fuzzy, c-format
+#, fuzzy
#~ msgid "Aborting payment ..."
#~ msgstr "Bekräftar betalning"
-#, fuzzy, c-format
-#~ msgid "Abort Payment"
-#~ msgstr "Godkän betalning"
-
-#, c-format
#~ msgid "Select"
#~ msgstr "Välj"
-#, fuzzy, c-format
-#~ msgid "The exchange is trusted by the wallet."
-#~ msgstr "Tjänsteleverantörer i plånboken:"
-
-#, fuzzy, c-format
+#, fuzzy
#~ msgid ""
#~ "Your wallet (protocol version %1$s) might be outdated.%2$s The exchange "
#~ "has a higher, incompatible protocol version (%3$s)."
#~ msgstr "tjänsteleverantörer plånboken"
-#, fuzzy, c-format
+#, fuzzy
#~ msgid ""
#~ "The chosen exchange (protocol version %1$s might be outdated.%2$s The "
#~ "exchange has a lower, incompatible protocol version than your wallet "
#~ "(protocol version %3$s)."
#~ msgstr "tjänsteleverantörer plånboken"
-#, fuzzy, c-format
+#, fuzzy
#~ msgid ""
#~ "Oops, something went wrong. The wallet responded with error status (%1$s)."
#~ msgstr "plånboken"
diff --git a/packages/taler-wallet-webextension/src/i18n/taler-wallet-webex.pot b/packages/taler-wallet-webextension/src/i18n/taler-wallet-webex.pot
index 67b09de1a..daf1460a2 100644
--- a/packages/taler-wallet-webextension/src/i18n/taler-wallet-webex.pot
+++ b/packages/taler-wallet-webextension/src/i18n/taler-wallet-webex.pot
@@ -1,21 +1,12 @@
-# This file is part of TALER
-# (C) 2016 GNUnet e.V.
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 3, or (at your option) any later version.
-#
-# TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: Taler Wallet\n"
+"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
@@ -25,266 +16,1937 @@ msgstr ""
"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/util/wire.ts:37
+#: src/NavigationBar.tsx:139
#, c-format
-msgid "Invalid Wire"
+msgid "Balance"
msgstr ""
-#: src/util/wire.ts:42 src/util/wire.ts:45
+#: src/NavigationBar.tsx:142
#, c-format
-msgid "Invalid Test Wire Detail"
+msgid "Backup"
msgstr ""
-#: src/util/wire.ts:47
+#: src/NavigationBar.tsx:147
#, c-format
-msgid "Test Wire Acct #%1$s on %2$s"
+msgid "QR Reader and Taler URI"
msgstr ""
-#: src/util/wire.ts:49
+#: src/NavigationBar.tsx:154
#, c-format
-msgid "Unknown Wire Detail"
+msgid "Settings"
msgstr ""
-#: src/webex/pages/benchmark.tsx:52
+#: src/NavigationBar.tsx:184
#, c-format
-msgid "Operation"
+msgid "Dev"
msgstr ""
-#: src/webex/pages/benchmark.tsx:53
+#: src/mui/Typography.tsx:122
#, c-format
-msgid "time (ms/op)"
+msgid "%1$s"
msgstr ""
-#: src/webex/pages/pay.tsx:130
+#: src/components/PendingTransactions.tsx:74
#, c-format
-msgid "The merchant %1$s offers you to purchase:"
+msgid "PENDING OPERATIONS"
msgstr ""
-#: src/webex/pages/pay.tsx:136
+#: src/components/Loading.tsx:36
#, c-format
-msgid "The total price is %1$s (plus %2$s fees)."
+msgid "Loading"
msgstr ""
-#: src/webex/pages/pay.tsx:141
+#: src/wallet/BackupPage.tsx:123
#, c-format
-msgid "The total price is %1$s."
+msgid "Could not load backup providers"
msgstr ""
-#: src/webex/pages/pay.tsx:163
+#: src/wallet/BackupPage.tsx:202
#, c-format
-msgid "Retry"
+msgid "No backup providers configured"
msgstr ""
-#: src/webex/pages/pay.tsx:173
+#: src/wallet/BackupPage.tsx:205
#, c-format
-msgid "Confirm payment"
+msgid "Add provider"
msgstr ""
-#: src/webex/pages/popup.tsx:153
+#: src/wallet/BackupPage.tsx:219
#, c-format
-msgid "Balance"
+msgid "Sync all backups"
msgstr ""
-#: src/webex/pages/popup.tsx:154
+#: src/wallet/BackupPage.tsx:221
#, c-format
-msgid "History"
+msgid "Sync now"
msgstr ""
-#: src/webex/pages/popup.tsx:155
+#: src/wallet/BackupPage.tsx:264
#, c-format
-msgid "Debug"
+msgid "Last synced"
msgstr ""
-#: src/webex/pages/popup.tsx:175
+#: src/wallet/BackupPage.tsx:269
#, c-format
-msgid "You have no balance to show. Need some %1$s getting started?"
+msgid "Not synced"
msgstr ""
-#: src/webex/pages/popup.tsx:238
+#: src/wallet/BackupPage.tsx:289
#, c-format
-msgid "%1$s incoming"
+msgid "Expires in"
msgstr ""
-#: src/webex/pages/popup.tsx:250
+#: src/wallet/ProviderDetailPage.tsx:60
#, c-format
-msgid "%1$s being spent"
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
msgstr ""
-#: src/webex/pages/popup.tsx:281
+#: src/wallet/ProviderDetailPage.tsx:108
#, c-format
-msgid "Error: could not retrieve balance information."
+msgid "There is not known provider with url &quot;%1$s&quot;."
msgstr ""
-#: src/webex/pages/popup.tsx:390
+#: src/wallet/ProviderDetailPage.tsx:115
#, c-format
-msgid "Invalid "
+msgid "See providers"
msgstr ""
-#: src/webex/pages/popup.tsx:396
+#: src/wallet/ProviderDetailPage.tsx:143
#, c-format
-msgid "Fees "
+msgid "Last backup"
msgstr ""
-#: src/webex/pages/popup.tsx:434
+#: src/wallet/ProviderDetailPage.tsx:148
#, c-format
-msgid "Refresh sessions has completed"
+msgid "Back up"
msgstr ""
-#: src/webex/pages/popup.tsx:451
+#: src/wallet/ProviderDetailPage.tsx:154
#, c-format
-msgid "Order Refused"
+msgid "Provider fee"
msgstr ""
-#: src/webex/pages/popup.tsx:465
+#: src/wallet/ProviderDetailPage.tsx:157
#, c-format
-msgid "Order redirected"
+msgid "per year"
msgstr ""
-#: src/webex/pages/popup.tsx:482
+#: src/wallet/ProviderDetailPage.tsx:163
#, c-format
-msgid "Payment aborted"
+msgid "Extend"
msgstr ""
-#: src/webex/pages/popup.tsx:512
+#: src/wallet/ProviderDetailPage.tsx:169
#, c-format
-msgid "Payment Sent"
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
msgstr ""
-#: src/webex/pages/popup.tsx:536
+#: src/wallet/ProviderDetailPage.tsx:179
#, c-format
-msgid "Order accepted"
+msgid "old"
msgstr ""
-#: src/webex/pages/popup.tsx:547
+#: src/wallet/ProviderDetailPage.tsx:183
#, c-format
-msgid "Reserve balance updated"
+msgid "new"
msgstr ""
-#: src/webex/pages/popup.tsx:559
+#: src/wallet/ProviderDetailPage.tsx:190
#, c-format
-msgid "Payment refund"
+msgid "fee"
msgstr ""
-#: src/webex/pages/popup.tsx:584
+#: src/wallet/ProviderDetailPage.tsx:198
#, c-format
-msgid "Withdrawn"
+msgid "storage"
msgstr ""
-#: src/webex/pages/popup.tsx:596
+#: src/wallet/ProviderDetailPage.tsx:215
#, c-format
-msgid "Tip Accepted"
+msgid "Remove provider"
msgstr ""
-#: src/webex/pages/popup.tsx:606
+#: src/wallet/ProviderDetailPage.tsx:228
#, c-format
-msgid "Tip Declined"
+msgid "This provider has reported an error"
msgstr ""
-#: src/webex/pages/popup.tsx:615
+#: src/wallet/ProviderDetailPage.tsx:242
#, c-format
-msgid "%1$s"
+msgid "There is conflict with another backup from %1$s"
msgstr ""
-#: src/webex/pages/popup.tsx:707
+#: src/wallet/ProviderDetailPage.tsx:253
#, c-format
-msgid "Your wallet has no events recorded."
+msgid "Backup is not readable"
msgstr ""
-#: src/webex/pages/return-coins.tsx:124
+#: src/wallet/ProviderDetailPage.tsx:261
#, c-format
-msgid "Wire to bank account"
+msgid "Unknown backup problem: %1$s"
msgstr ""
-#: src/webex/pages/return-coins.tsx:206
+#: src/wallet/ProviderDetailPage.tsx:283
#, c-format
-msgid "Confirm"
+msgid "service paid"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
msgstr ""
-#: src/webex/pages/return-coins.tsx:209
+#: src/wallet/AddNewActionView.tsx:57
#, c-format
msgid "Cancel"
msgstr ""
-#: src/webex/pages/withdraw.tsx:73
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr ""
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr ""
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr ""
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr ""
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
#, c-format
-msgid "Could not get details for withdraw operation:"
+msgid "Refund"
msgstr ""
-#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#: src/wallet/Transaction.tsx:555
#, c-format
-msgid "Chose different exchange provider"
+msgid "Original order ID"
msgstr ""
-#: src/webex/pages/withdraw.tsx:109
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
#, c-format
msgid ""
-"Please select an exchange. You can review the details before after your "
-"selection."
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
msgstr ""
-#: src/webex/pages/withdraw.tsx:121
+#: src/cta/Tip/views.tsx:74
#, c-format
-msgid "Select %1$s"
+msgid "Merchant URL"
msgstr ""
-#: src/webex/pages/withdraw.tsx:143
+#: src/cta/Tip/views.tsx:90
#, c-format
-msgid "Select custom exchange"
+msgid "Receive &nbsp; %1$s"
msgstr ""
-#: src/webex/pages/withdraw.tsx:163
+#: src/cta/Tip/views.tsx:114
#, c-format
-msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
msgstr ""
-#: src/webex/pages/withdraw.tsx:174
+#: src/components/SelectList.tsx:66
#, c-format
-msgid "Accept fees and withdraw"
+msgid "Select one option"
msgstr ""
-#: src/webex/pages/withdraw.tsx:192
+#: src/components/TermsOfService/views.tsx:39
#, c-format
-msgid "Cancel withdraw operation"
+msgid "Could not load"
msgstr ""
-#: src/webex/renderHtml.tsx:249
+#: src/components/TermsOfService/views.tsx:73
#, c-format
-msgid "Withdrawal fees:"
+msgid "Show terms of service"
msgstr ""
-#: src/webex/renderHtml.tsx:252
+#: src/components/TermsOfService/views.tsx:81
#, c-format
-msgid "Rounding loss:"
+msgid "I accept the exchange terms of service"
msgstr ""
-#: src/webex/renderHtml.tsx:254
+#: src/components/TermsOfService/views.tsx:107
#, c-format
-msgid "Earliest expiration (for deposit): %1$s"
+msgid "Exchange doesn&apos;t have terms of service"
msgstr ""
-#: src/webex/renderHtml.tsx:262
+#: src/components/TermsOfService/views.tsx:135
#, c-format
-msgid "# Coins"
+msgid "Review exchange terms of service"
msgstr ""
-#: src/webex/renderHtml.tsx:263
+#: src/components/TermsOfService/views.tsx:146
#, c-format
-msgid "Value"
+msgid "Review new version of terms of service"
msgstr ""
-#: src/webex/renderHtml.tsx:264
+#: src/components/TermsOfService/views.tsx:170
#, c-format
-msgid "Withdraw Fee"
+msgid "The exchange reply with a empty terms of service"
msgstr ""
-#: src/webex/renderHtml.tsx:265
+#: src/components/TermsOfService/views.tsx:193
#, c-format
-msgid "Refresh Fee"
+msgid "Download Terms of Service"
msgstr ""
-#: src/webex/renderHtml.tsx:266
+#: src/components/TermsOfService/views.tsx:204
#, c-format
-msgid "Deposit Fee"
+msgid "Hide terms of service"
msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
+
diff --git a/packages/taler-wallet-webextension/src/i18n/tr.po b/packages/taler-wallet-webextension/src/i18n/tr.po
new file mode 100644
index 000000000..5848b9f3a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/tr.po
@@ -0,0 +1,2087 @@
+# This file is part of GNU Taler
+# (C) 2022 Taler Systems S.A.
+#
+# GNU Taler is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3, or (at your option) any later version.
+#
+# GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-08 01:14+0000\n"
+"Last-Translator: Alp <berna.alp@digitalekho.com>\n"
+"Language-Team: Turkish <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/tr/>\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Bakiye"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "Yedekle"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr ""
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Ayarlar"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr "Gelişim"
+
+#: src/mui/Typography.tsx:122
+#, fuzzy, c-format
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr ""
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Yükleniyor"
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr "Yedekleme sağlayıcıları yüklenemedi"
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr "Yapılandırılmış yedekleme sağlayıcısı yok"
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr "Sağlayıcı ekle"
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr "Tüm yedeklemeleri senkronize et"
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr "Şimdi senkronize et"
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr "Son Senkronizasyon"
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr "Senkronize Edilmedi"
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr "İçinde sona eriyor"
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, fuzzy, c-format
+msgid "See providers"
+msgstr "Sağlayıcı ekle"
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms "
+"of service"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr ""
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr "Bilinmeyen yedekleme problemi: %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr "hizmet ödendi"
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr "Yedekleme geçerlilik süresi"
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "İptal et"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Rezerv sayfasını açın"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Ödeme sayfasını açın"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Geri ödeme sayfasını açın"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "İkramiye sayfasını açın"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Para çekme sayfasını açın"
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr "Dijital para alın"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr "Ekle"
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr "%1$s gönder"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr "Taler Eylemi"
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr "Bu sayfada ödeme eylemi var."
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr "Bu sayfada para çekme eylemi var."
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr "Bu sayfada bir ikramiye eylemi var."
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr "Bu sayfada bir rezervasyon bildir eylemi var."
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr "Bildirin"
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr "Bu sayfada bir geri ödeme eylemi var."
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr "Bu sayfada hatalı biçimlendirilmiş taler uri var."
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr "Reddet"
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, fuzzy, c-format
+msgid "Could not load purchase proposal details"
+msgstr "Yedekleme sağlayıcıları yüklenemedi"
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, fuzzy, c-format
+msgid "Order Id"
+msgstr "Sipariş reddedildi"
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr "Satıcı web sitesi"
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr "Satıcı e-postası"
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, fuzzy, c-format
+msgid "Delivery location"
+msgstr "Taler Eylemi"
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, fuzzy, c-format
+msgid "Auto refund"
+msgstr "Ödeme iadesi"
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, fuzzy, c-format
+msgid "Exchanges"
+msgstr "Exchange"
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:38
+#, fuzzy, c-format
+msgid "Could not load deposit status"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr "Ücretler"
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an "
+"minimum amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two "
+"additional recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid ""
+"Make sure the amount show %1$s BTC, else you have to change the base unit to "
+"BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr "Yeniden deneyin"
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to "
+"get the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr "Onaylamak"
+
+#: src/wallet/Transaction.tsx:267
+#, fuzzy, c-format
+msgid "Withdrawal"
+msgstr "Çekildi"
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and "
+"check there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid ""
+"Bank has confirmed the wire transfer. Waiting for the exchange to send the "
+"coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, fuzzy, c-format
+msgid "Payment"
+msgstr "Ödeme gönderildi"
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid ""
+"Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, fuzzy, c-format
+msgid "Accept"
+msgstr "İkramiye kabul edildi"
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, fuzzy, c-format
+msgid "Refresh"
+msgstr "Ücreti yenile"
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr "Exchange"
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr "Para çek"
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, fuzzy, c-format
+msgid "Total transfer"
+msgstr "Çekildi"
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, fuzzy, c-format
+msgid "Your current balance is not enough."
+msgstr "Gösterecek bakiyeniz yok."
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, fuzzy, c-format
+msgid "Could not load refund status"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, fuzzy, c-format
+msgid "Total to refund"
+msgstr "Ödeme iadesi"
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, fuzzy, c-format
+msgid "Order amount"
+msgstr "Ücreti yenile"
+
+#: src/cta/Refund/views.tsx:122
+#, fuzzy, c-format
+msgid "Already refunded"
+msgstr "Ödeme iadesi"
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, fuzzy, c-format
+msgid "Could not load tip status"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, fuzzy, c-format
+msgid "Could not load"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr "Hizmet şartlarını göster"
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr "Hizmet şartlarını kabul ediyorum"
+
+#: src/components/TermsOfService/views.tsx:107
+#, fuzzy, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr "Exchange'in hizmet şartları yok"
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr "Exchange'in hizmet şartlarını inceleyin"
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr "Hizmet şartlarının yeni sürümünü inceleyin"
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, fuzzy, c-format
+msgid "Could not load exchange fees"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, fuzzy, c-format
+msgid "could not find any exchange"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, fuzzy, c-format
+msgid "Select %1$s exchange"
+msgstr "Özel exchange'i seçin"
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, fuzzy, c-format
+msgid "Use this exchange"
+msgstr "Özel exchange'i seçin"
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, fuzzy, c-format
+msgid "Operations"
+msgstr "Bekleyen işlemler"
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, fuzzy, c-format
+msgid "Deposits"
+msgstr "Depozito %1$s"
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, fuzzy, c-format
+msgid "Denomination"
+msgstr "Bekleyen işlemler"
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, fuzzy, c-format
+msgid "Withdrawals"
+msgstr "Çekildi"
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, fuzzy, c-format
+msgid "Coin operations"
+msgstr "Bekleyen işlemler"
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and "
+"is valid for a period of time. The exchange will charge the indicated amount "
+"every time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, fuzzy, c-format
+msgid "Transfer operations"
+msgstr "Bekleyen işlemler"
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is "
+"valid for a period of time. The exchange will charge the indicated amount "
+"every time a transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, fuzzy, c-format
+msgid "Wallet operations"
+msgstr "Bekleyen işlemler"
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+# #: src/wallet/Transaction.tsx:527
+# #, c-format
+# msgid "Cancel"
+# msgstr "İptal et"
+#: src/cta/Withdraw/views.tsx:60
+#, fuzzy, c-format
+msgid "Could not get info of withdrawal"
+msgstr "Para çekme işlemi için ayrıntılar alınamadı:"
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+# #: src/wallet/Transaction.tsx:527
+# #, c-format
+# msgid "Cancel"
+# msgstr "İptal et"
+#: src/cta/InvoicePay/views.tsx:63
+#, fuzzy, c-format
+msgid "Could not finish the payment operation"
+msgstr "Para çekme işlemi için ayrıntılar alınamadı:"
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+# #: src/wallet/Transaction.tsx:527
+# #, c-format
+# msgid "Cancel"
+# msgstr "İptal et"
+#: src/cta/TransferCreate/views.tsx:59
+#, fuzzy, c-format
+msgid "Could not finish the transfer creation"
+msgstr "Para çekme işlemi için ayrıntılar alınamadı:"
+
+# #: src/wallet/Transaction.tsx:527
+# #, c-format
+# msgid "Cancel"
+# msgstr "İptal et"
+#: src/cta/TransferPickup/views.tsx:57
+#, fuzzy, c-format
+msgid "Could not finish the pickup operation"
+msgstr "Para çekme işlemi için ayrıntılar alınamadı:"
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, fuzzy, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr "Para çekme ücretleri:"
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will "
+"send the coins to this wallet after receiving a wire transfer with the "
+"correct subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, fuzzy, c-format
+msgid "Could not load deposit balance"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, fuzzy, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr "Test Havale Hesap #%1$s üzerinde %2$s"
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, fuzzy, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr "Hizmet şartlarını kabul ediyorum"
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid ""
+"To complete the process you need to wire%1$s %2$s to the exchange bank "
+"account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a "
+"banking app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, fuzzy, c-format
+msgid "Could not toggle auto-open"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/wallet/Settings.tsx:121
+#, fuzzy, c-format
+msgid "Could not toggle clipboard"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires "
+"more permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr "Tanılar zaman aşımına uğradı. Cüzdan arka ucuyla konuşulamadı."
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr "Tespit edilen sorunlar:"
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check "
+"the preference name %2$s)."
+msgstr ""
+"Lütfen %1$s ayarlarınızda, IndexedDB'nin etkinleştirildiğinizi kontrol edin "
+"(%2$s tercih adını kontrol edin)."
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+"Cüzdan veritabanınız eski. Şu anda otomatik aktarım desteklenmiyor. Cüzdan "
+"veritabanını sıfırlamak için lütfen %1$s gidin."
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr "Tanılamayı çalıştır"
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr "Hata ayıklama araçları"
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE "
+"ALL YOUR COINS?"
+msgstr ""
+"Cüzdanınızdaki her şeyi GERİ ALINAMAZ BİÇİMDE İMHA ETMEK ve TÜM PARALARINIZI "
+"KAYBETMEK mi istiyorsunuz?"
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr "sıfırla"
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr "veritabanını içe aktar"
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr "veritabanını dışa aktar"
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr "Veritabanı %1$s'de dışa aktarıldı-%2$s indirilecek"
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr "Madeni paralar"
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr "Bekleyen işlemler"
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr "kullanılabilir madeni paralar"
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr "kimlik"
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr "değer"
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr "durum"
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr "yenilemeden mi?"
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr "harcanan madeni paralar"
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr "göstermek için tıklayın"
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Açık"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, fuzzy, c-format
+msgid "Could not load list of exchange"
+msgstr "Bakiye sayfası yüklenemedi"
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, fuzzy, c-format
+msgid "From my bank account"
+msgstr "Banka hesabına havale yap"
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, fuzzy, c-format
+msgid "To my bank account"
+msgstr "Banka hesabına havale yap"
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, fuzzy, c-format
+msgid "Could not load backup recovery information"
+msgstr "Yedekleme sağlayıcıları yüklenemedi"
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+"Lütfen bir exchange seçin. Detayları seçiminizden önce inceleyebilirsiniz."
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
+
+#~ msgid "Back"
+#~ msgstr "Geri"
+
+#~ msgid ""
+#~ "To withdraw money you can start from your bank site or click the "
+#~ "\"withdraw\" button to use a known exchange."
+#~ msgstr ""
+#~ "Para çekmek için banka sitenizden başlayabilir veya bilinen bir "
+#~ "exchange'i kullanmak için \"para çek\" düğmesini tıklayabilirsiniz."
+
+#~ msgid "Enter URI"
+#~ msgstr "URI'yi girin"
+
+#, fuzzy
+#~ msgid "no balance"
+#~ msgstr "Bakiye"
+
+#, fuzzy
+#~ msgid "Deposit amount"
+#~ msgstr "Depozito Ücreti"
+
+#~ msgid "Debug"
+#~ msgstr "Hata ayıklama"
+
+#, fuzzy
+#~ msgid "You have no balance to show. Need some %1$s getting started?"
+#~ msgstr "Gösterecek bakiyeniz yok. Başlamak için %1$s'ye mi ihtiyacınız var?"
+
+#, fuzzy
+#~ msgid "%1$s incoming"
+#~ msgstr "%1$s gelen"
+
+#, fuzzy
+#~ msgid "%1$s being spent"
+#~ msgstr "%1$s harcanan"
+
+#~ msgid "Error: could not retrieve balance information."
+#~ msgstr "Hata: bakiye bilgisi alınamadı."
+
+#~ msgid "Invalid "
+#~ msgstr "Geçersiz "
+
+#~ msgid "Refresh sessions has completed"
+#~ msgstr "Oturumların yenilenmesi tamamlandı"
+
+#~ msgid "Order redirected"
+#~ msgstr "Sipariş yönlendirildi"
+
+#~ msgid "Payment aborted"
+#~ msgstr "Ödeme durduruldu"
+
+#~ msgid "Order accepted"
+#~ msgstr "Sipariş kabul edildi"
+
+#~ msgid "Reserve balance updated"
+#~ msgstr "Yedek bakiye güncellendi"
+
+#, fuzzy
+#~ msgid "Tip Declined"
+#~ msgstr "İkramiye red edildi"
+
+#~ msgid "Your wallet has no events recorded."
+#~ msgstr "Cüzdanınızda kayıtlı bir haraket yok."
+
+#~ msgid "Chose different exchange provider"
+#~ msgstr "Farklı bir exchange sağlayıcısı seçin"
+
+#, fuzzy
+#~ msgid ""
+#~ "You are about to withdraw %1$s from your bank account into your wallet."
+#~ msgstr "Banka hesabınızdan cüzdanınıza %1$s çekmek üzeresiniz."
+
+#~ msgid "Accept fees and withdraw"
+#~ msgstr "Ücretleri kabul edin ve para çekin"
+
+#~ msgid "Cancel withdraw operation"
+#~ msgstr "Para çekme işlemini iptal edin"
+
+#~ msgid "Rounding loss:"
+#~ msgstr "Yuvarlama kaybı:"
+
+#, fuzzy
+#~ msgid "Earliest expiration (for deposit): %1$s"
+#~ msgstr "En erken sona erme (depozito için): %1$s"
+
+#, fuzzy
+#~ msgid "# Coins"
+#~ msgstr "# Madeni para"
+
+#~ msgid "Value"
+#~ msgstr "Değer"
+
+#~ msgid "Withdraw Fee"
+#~ msgstr "Para çekme Ücreti"
diff --git a/packages/taler-wallet-webextension/src/i18n/uk.po b/packages/taler-wallet-webextension/src/i18n/uk.po
new file mode 100644
index 000000000..c4f5d6537
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n/uk.po
@@ -0,0 +1,1956 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: languages@taler.net\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-03-05 13:03+0000\n"
+"Last-Translator: Tim Vutor <flukes.ostrich0p@icloud.com>\n"
+"Language-Team: Ukrainian <https://weblate.taler.net/projects/gnu-taler/"
+"webextensions/uk/>\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/NavigationBar.tsx:139
+#, c-format
+msgid "Balance"
+msgstr "Баланс"
+
+#: src/NavigationBar.tsx:142
+#, c-format
+msgid "Backup"
+msgstr "Бекап"
+
+#: src/NavigationBar.tsx:147
+#, c-format
+msgid "QR Reader and Taler URI"
+msgstr "QR-читалка та Taler URI"
+
+#: src/NavigationBar.tsx:154
+#, c-format
+msgid "Settings"
+msgstr "Налаштування"
+
+#: src/NavigationBar.tsx:184
+#, c-format
+msgid "Dev"
+msgstr "Розробка"
+
+#: src/mui/Typography.tsx:122
+#, c-format, fuzzy
+msgid "%1$s"
+msgstr "%1$s"
+
+#: src/components/PendingTransactions.tsx:74
+#, c-format
+msgid "PENDING OPERATIONS"
+msgstr "НЕЗАВЕРШЕНІ ОПЕРАЦІЇ"
+
+#: src/components/Loading.tsx:36
+#, c-format
+msgid "Loading"
+msgstr "Завантаження"
+
+#: src/wallet/BackupPage.tsx:123
+#, c-format
+msgid "Could not load backup providers"
+msgstr "Не вдалося завантажити зберігачів резервних копій"
+
+#: src/wallet/BackupPage.tsx:202
+#, c-format
+msgid "No backup providers configured"
+msgstr "Не налаштовано жодного зберігача резервних копій"
+
+#: src/wallet/BackupPage.tsx:205
+#, c-format
+msgid "Add provider"
+msgstr "Додати зберігача"
+
+#: src/wallet/BackupPage.tsx:219
+#, c-format
+msgid "Sync all backups"
+msgstr "Синхронізувати всі резервні копії"
+
+#: src/wallet/BackupPage.tsx:221
+#, c-format
+msgid "Sync now"
+msgstr "Синхронізувати зараз"
+
+#: src/wallet/BackupPage.tsx:264
+#, c-format
+msgid "Last synced"
+msgstr "Останній раз синхронізовано"
+
+#: src/wallet/BackupPage.tsx:269
+#, c-format
+msgid "Not synced"
+msgstr "Не синхронізовано"
+
+#: src/wallet/BackupPage.tsx:289
+#, c-format
+msgid "Expires in"
+msgstr "Термін дії закінчується в"
+
+#: src/wallet/ProviderDetailPage.tsx:60
+#, c-format
+msgid "There was an error loading the provider detail for &quot; %1$s&quot;"
+msgstr "Виникла помилка при завантаженні інформації зберігача &quot; %1$s&quot;"
+
+#: src/wallet/ProviderDetailPage.tsx:108
+#, c-format
+msgid "There is not known provider with url &quot;%1$s&quot;."
+msgstr "Зберігач з посиланням &quot;%1$s&quot; невідомий."
+
+#: src/wallet/ProviderDetailPage.tsx:115
+#, c-format
+msgid "See providers"
+msgstr "Подивитись зберігачів"
+
+#: src/wallet/ProviderDetailPage.tsx:143
+#, c-format
+msgid "Last backup"
+msgstr "Остання резервна копія"
+
+#: src/wallet/ProviderDetailPage.tsx:148
+#, c-format
+msgid "Back up"
+msgstr "Зробити резервну копію"
+
+#: src/wallet/ProviderDetailPage.tsx:154
+#, c-format
+msgid "Provider fee"
+msgstr "Комісія зберігача"
+
+#: src/wallet/ProviderDetailPage.tsx:157
+#, c-format
+msgid "per year"
+msgstr "на рік"
+
+#: src/wallet/ProviderDetailPage.tsx:163
+#, c-format
+msgid "Extend"
+msgstr "Подовжити"
+
+#: src/wallet/ProviderDetailPage.tsx:169
+#, c-format
+msgid ""
+"terms has changed, extending the service will imply accepting the new terms of "
+"service"
+msgstr ""
+"умови надання послуг змінились, продовження послуги означатиме прийняття "
+"нових умов"
+
+#: src/wallet/ProviderDetailPage.tsx:179
+#, c-format
+msgid "old"
+msgstr "старий"
+
+#: src/wallet/ProviderDetailPage.tsx:183
+#, c-format
+msgid "new"
+msgstr "новий"
+
+#: src/wallet/ProviderDetailPage.tsx:190
+#, c-format
+msgid "fee"
+msgstr "комісія"
+
+#: src/wallet/ProviderDetailPage.tsx:198
+#, c-format
+msgid "storage"
+msgstr "сховище"
+
+#: src/wallet/ProviderDetailPage.tsx:215
+#, c-format
+msgid "Remove provider"
+msgstr "Видалити зберігача"
+
+#: src/wallet/ProviderDetailPage.tsx:228
+#, c-format
+msgid "This provider has reported an error"
+msgstr "Цей постачальник повідомив про помилку"
+
+#: src/wallet/ProviderDetailPage.tsx:242
+#, c-format
+msgid "There is conflict with another backup from %1$s"
+msgstr "Конфлікт з іншою резервною копією з %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:253
+#, c-format
+msgid "Backup is not readable"
+msgstr "Резервна копія пошкоджена або не може бути прочитана"
+
+#: src/wallet/ProviderDetailPage.tsx:261
+#, c-format
+msgid "Unknown backup problem: %1$s"
+msgstr "Невідома помилка резервного копіювання: %1$s"
+
+#: src/wallet/ProviderDetailPage.tsx:283
+#, c-format
+msgid "service paid"
+msgstr "послуга сплачена"
+
+#: src/wallet/ProviderDetailPage.tsx:290
+#, c-format
+msgid "Backup valid until"
+msgstr "Резервна копія дійсна до"
+
+#: src/wallet/AddNewActionView.tsx:57
+#, c-format
+msgid "Cancel"
+msgstr "Відмінити"
+
+#: src/wallet/AddNewActionView.tsx:68
+#, c-format
+msgid "Open reserve page"
+msgstr "Показати резерв"
+
+#: src/wallet/AddNewActionView.tsx:70
+#, c-format
+msgid "Open pay page"
+msgstr "Показати сторінку оплати"
+
+#: src/wallet/AddNewActionView.tsx:72
+#, c-format
+msgid "Open refund page"
+msgstr "Показати відшкодування"
+
+#: src/wallet/AddNewActionView.tsx:74
+#, c-format
+msgid "Open tip page"
+msgstr "Показати чайові"
+
+#: src/wallet/AddNewActionView.tsx:76
+#, c-format
+msgid "Open withdraw page"
+msgstr "Показати списання"
+
+#: src/popup/NoBalanceHelp.tsx:43
+#, c-format
+msgid "Get digital cash"
+msgstr "Отримати е-готівку"
+
+#: src/popup/BalancePage.tsx:138
+#, c-format
+msgid "Could not load balance page"
+msgstr "Не вдалося показати залишок"
+
+#: src/popup/BalancePage.tsx:175
+#, c-format
+msgid "Add"
+msgstr "Додати"
+
+#: src/popup/BalancePage.tsx:179
+#, c-format
+msgid "Send %1$s"
+msgstr "Переказати %1$s"
+
+#: src/popup/TalerActionFound.tsx:44
+#, c-format
+msgid "Taler Action"
+msgstr "Taler Дія"
+
+#: src/popup/TalerActionFound.tsx:49
+#, c-format
+msgid "This page has pay action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:63
+#, c-format
+msgid "This page has a withdrawal action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:79
+#, c-format
+msgid "This page has a tip action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:93
+#, c-format
+msgid "This page has a notify reserve action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:102
+#, c-format
+msgid "Notify"
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:109
+#, c-format
+msgid "This page has a refund action."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:123
+#, c-format
+msgid "This page has a malformed taler uri."
+msgstr ""
+
+#: src/popup/TalerActionFound.tsx:134
+#, c-format
+msgid "Dismiss"
+msgstr ""
+
+#: src/popup/Application.tsx:177
+#, c-format
+msgid "this popup is being closed and you are being redirected to %1$s"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:158
+#, c-format
+msgid "Could not load purchase proposal details"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:183
+#, c-format
+msgid "Order Id"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:189
+#, c-format
+msgid "Summary"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:195
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:203
+#, c-format
+msgid "Merchant name"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:209
+#, c-format
+msgid "Merchant jurisdiction"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:215
+#, c-format
+msgid "Merchant address"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:221
+#, c-format
+msgid "Merchant logo"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:234
+#, c-format
+msgid "Merchant website"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:240
+#, c-format
+msgid "Merchant email"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:246
+#, c-format
+msgid "Merchant public key"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:256
+#, c-format
+msgid "Delivery date"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:271
+#, c-format
+msgid "Delivery location"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:277
+#, c-format
+msgid "Products"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:289
+#, c-format
+msgid "Created at"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:304
+#, c-format
+msgid "Refund deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:319
+#, c-format
+msgid "Auto refund"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:339
+#, c-format
+msgid "Pay deadline"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:354
+#, c-format
+msgid "Fulfillment URL"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:360
+#, c-format
+msgid "Fulfillment message"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:370
+#, c-format
+msgid "Max deposit fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:378
+#, c-format
+msgid "Max fee"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:386
+#, c-format
+msgid "Minimum age"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:398
+#, c-format
+msgid "Wire fee amortization"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:404
+#, c-format
+msgid "Auditors"
+msgstr ""
+
+#: src/components/ShowFullContractTermPopup.tsx:419
+#, c-format
+msgid "Exchanges"
+msgstr ""
+
+#: src/components/Part.tsx:148
+#, c-format
+msgid "Bank account"
+msgstr ""
+
+#: src/components/Part.tsx:160
+#, c-format
+msgid "Bitcoin address"
+msgstr ""
+
+#: src/components/Part.tsx:163
+#, c-format
+msgid "IBAN"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:38
+#, c-format
+msgid "Could not load deposit status"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:52
+#, c-format
+msgid "Digital cash deposit"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:58
+#, c-format
+msgid "Cost"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:66
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:73
+#, c-format
+msgid "To be received"
+msgstr ""
+
+#: src/cta/Deposit/views.tsx:84
+#, c-format
+msgid "Send &nbsp; %1$s"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:63
+#, c-format
+msgid "Bitcoin transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:66
+#, c-format
+msgid ""
+"The exchange need a transaction with 3 output, one output is the exchange "
+"account and the other two are segwit fake address for metadata with an minimum "
+"amount."
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:74
+#, c-format
+msgid ""
+"In bitcoincore wallet use &apos;Add Recipient&apos; button to add two additional "
+"recipient and copy addresses and amounts"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:98
+#, c-format
+msgid "Make sure the amount show %1$s BTC, else you have to change the base unit to BTC"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:110
+#, c-format
+msgid "Account"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:116
+#, c-format
+msgid "Bank host"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:139
+#, c-format
+msgid "Bank transfer details"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:148
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/BankDetailsByPaytoType.tsx:154
+#, c-format
+msgid "Receiver name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:98
+#, c-format
+msgid "Could not load the transaction information"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:191
+#, c-format
+msgid "There was an error trying to complete the transaction"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:200
+#, c-format
+msgid "This transaction is not completed"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:209
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:216
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:224
+#, c-format
+msgid "Forget"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:241
+#, c-format
+msgid "Caution!"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:244
+#, c-format
+msgid ""
+"If you have already wired money to the exchange you will loose the chance to get "
+"the coins form it."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:259
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:267
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:286
+#, c-format
+msgid ""
+"Make sure to use the correct subject, otherwise the money will not arrive in "
+"this wallet."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:298
+#, c-format
+msgid ""
+"The bank did not yet confirmed the wire transfer. Go to the %1$s %2$s and check "
+"there is no pending step."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:316
+#, c-format
+msgid "Bank has confirmed the wire transfer. Waiting for the exchange to send the coins"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:325
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:360
+#, c-format
+msgid "Payment"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:378
+#, c-format
+msgid "Refunds"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:385
+#, c-format
+msgid "%1$s %2$s on %3$s"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:415
+#, c-format
+msgid "Merchant created a refund for this order but was not automatically picked up."
+msgstr ""
+
+#: src/wallet/Transaction.tsx:420
+#, c-format
+msgid "Offer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:431
+#, c-format
+msgid "Accept"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:438
+#, c-format
+msgid "Merchant"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:443
+#, c-format
+msgid "Invoice ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:470
+#, c-format
+msgid "Deposit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:496
+#, c-format
+msgid "Refresh"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:517
+#, c-format
+msgid "Tip"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:542
+#, c-format
+msgid "Refund"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:555
+#, c-format
+msgid "Original order ID"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:568
+#, c-format
+msgid "Purchase summary"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:593
+#, c-format
+msgid "copy"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:596
+#, c-format
+msgid "hide qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:608
+#, c-format
+msgid "show qr"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:620
+#, c-format
+msgid "Credit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:624
+#, c-format
+msgid "Invoice"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:635
+#, c-format
+msgid "Exchange"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:641
+#, c-format
+msgid "URI"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:667
+#, c-format
+msgid "Debit"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:710
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:844
+#, c-format
+msgid "Country"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:852
+#, c-format
+msgid "Address lines"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:860
+#, c-format
+msgid "Building number"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:868
+#, c-format
+msgid "Building name"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:876
+#, c-format
+msgid "Street"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:884
+#, c-format
+msgid "Post code"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:892
+#, c-format
+msgid "Town location"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:900
+#, c-format
+msgid "Town"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:908
+#, c-format
+msgid "District"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:916
+#, c-format
+msgid "Country subdivision"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:935
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:990
+#, c-format
+msgid "Transaction fees"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1004
+#, c-format
+msgid "Total"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1074
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1146
+#, c-format
+msgid "Price"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1156
+#, c-format
+msgid "Refunded"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1220
+#, c-format
+msgid "Delivery"
+msgstr ""
+
+#: src/wallet/Transaction.tsx:1335
+#, c-format
+msgid "Total transfer"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:57
+#, c-format
+msgid "Could not load pay status"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:87
+#, c-format
+msgid "Digital cash payment"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:119
+#, c-format
+msgid "Purchase"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:149
+#, c-format
+msgid "Receipt"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:156
+#, c-format
+msgid "Valid until"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:191
+#, c-format
+msgid "List of products"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:242
+#, c-format
+msgid "free"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:263
+#, c-format
+msgid "Already paid, you are going to be redirected to %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:274
+#, c-format
+msgid "Already paid"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:280
+#, c-format
+msgid "Already claimed"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:296
+#, c-format
+msgid "Pay with a mobile phone"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:298
+#, c-format
+msgid "Hide QR"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:305
+#, c-format
+msgid "Scan the QR code or &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:346
+#, c-format
+msgid "Pay &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Payment/views.tsx:360
+#, c-format
+msgid "You have no balance for this currency. Withdraw digital cash first."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:364
+#, c-format
+msgid ""
+"Could not find enough coins to pay. Even if you have enough %1$s some "
+"restriction may apply."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:366
+#, c-format
+msgid "Your current balance is not enough."
+msgstr ""
+
+#: src/cta/Payment/views.tsx:395
+#, c-format
+msgid "Merchant message"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:34
+#, c-format
+msgid "Could not load refund status"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:48
+#, c-format
+msgid "Digital cash refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:52
+#, c-format
+msgid "You&apos;ve ignored the tip."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:70
+#, c-format
+msgid "The refund is in progress."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:76
+#, c-format
+msgid "Total to refund"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:106
+#, c-format
+msgid "The merchant &quot;%1$s&quot; is offering you a refund."
+msgstr ""
+
+#: src/cta/Refund/views.tsx:115
+#, c-format
+msgid "Order amount"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:122
+#, c-format
+msgid "Already refunded"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:129
+#, c-format
+msgid "Refund offered"
+msgstr ""
+
+#: src/cta/Refund/views.tsx:145
+#, c-format
+msgid "Accept &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:32
+#, c-format
+msgid "Could not load tip status"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:45
+#, c-format
+msgid "Digital cash tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:66
+#, c-format
+msgid "The merchant is offering you a tip"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:74
+#, c-format
+msgid "Merchant URL"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:90
+#, c-format
+msgid "Receive &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Tip/views.tsx:114
+#, c-format
+msgid "Tip from %1$s accepted. Check your transactions list for more details."
+msgstr ""
+
+#: src/components/SelectList.tsx:66
+#, c-format
+msgid "Select one option"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:39
+#, c-format
+msgid "Could not load"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:73
+#, c-format
+msgid "Show terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:81
+#, c-format
+msgid "I accept the exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:107
+#, c-format
+msgid "Exchange doesn&apos;t have terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:135
+#, c-format
+msgid "Review exchange terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:146
+#, c-format
+msgid "Review new version of terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:170
+#, c-format
+msgid "The exchange reply with a empty terms of service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:193
+#, c-format
+msgid "Download Terms of Service"
+msgstr ""
+
+#: src/components/TermsOfService/views.tsx:204
+#, c-format
+msgid "Hide terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:117
+#, c-format
+msgid "Could not load exchange fees"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:131
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:160
+#, c-format
+msgid "could not find any exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:166
+#, c-format
+msgid "could not find any exchange for the currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:186
+#, c-format
+msgid "Service fee description"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:201
+#, c-format
+msgid "Select %1$s exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:215
+#, c-format
+msgid "Reset"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:218
+#, c-format
+msgid "Use this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:230
+#, c-format
+msgid "Doesn&apos;t have auditors"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:241
+#, c-format
+msgid "currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:249
+#, c-format
+msgid "Operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:252
+#, c-format
+msgid "Deposits"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:259
+#, c-format
+msgid "Denomination"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:265
+#, c-format
+msgid "Until"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:274
+#, c-format
+msgid "Withdrawals"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:423
+#, c-format
+msgid "Currency"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:433
+#, c-format
+msgid "Coin operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:436
+#, c-format
+msgid ""
+"Every operation in this section may be different by denomination value and is "
+"valid for a period of time. The exchange will charge the indicated amount every "
+"time a coin is used in such operation."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:545
+#, c-format
+msgid "Transfer operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:548
+#, c-format
+msgid ""
+"Every operation in this section may be different by transfer type and is valid "
+"for a period of time. The exchange will charge the indicated amount every time a "
+"transfer is made."
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:563
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:583
+#, c-format
+msgid "Wallet operations"
+msgstr ""
+
+#: src/wallet/ExchangeSelection/views.tsx:597
+#, c-format
+msgid "Feature"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:47
+#, c-format
+msgid "Could not get the info from the URI"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:60
+#, c-format
+msgid "Could not get info of withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:74
+#, c-format
+msgid "Digital cash withdrawal"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:79
+#, c-format
+msgid "Could not finish the withdrawal operation"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:127
+#, c-format
+msgid "Age restriction"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:145
+#, c-format
+msgid "Withdraw &nbsp; %1$s"
+msgstr ""
+
+#: src/cta/Withdraw/views.tsx:179
+#, c-format
+msgid "Withdraw to a mobile phone"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:65
+#, c-format
+msgid "Digital invoice"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:69
+#, c-format
+msgid "Could not finish the invoice creation"
+msgstr ""
+
+#: src/cta/InvoiceCreate/views.tsx:130
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/cta/InvoicePay/views.tsx:63
+#, c-format
+msgid "Could not finish the payment operation"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:55
+#, c-format
+msgid "Digital cash transfer"
+msgstr ""
+
+#: src/cta/TransferCreate/views.tsx:59
+#, c-format
+msgid "Could not finish the transfer creation"
+msgstr ""
+
+#: src/cta/TransferPickup/views.tsx:57
+#, c-format
+msgid "Could not finish the pickup operation"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:149
+#, c-format
+msgid "Manual Withdrawal for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:154
+#, c-format
+msgid ""
+"Choose a exchange from where the coins will be withdrawn. The exchange will send "
+"the coins to this wallet after receiving a wire transfer with the correct "
+"subject."
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:162
+#, c-format
+msgid "No exchange found for %1$s"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:170
+#, c-format
+msgid "Add Exchange"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:192
+#, c-format
+msgid "No exchange configured"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:210
+#, c-format
+msgid "Can&apos;t create the reserve"
+msgstr ""
+
+#: src/wallet/CreateManualWithdraw.tsx:277
+#, c-format
+msgid "Start withdrawal"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:38
+#, c-format
+msgid "Could not load deposit balance"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:51
+#, c-format
+msgid "A currency or an amount should be indicated"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:67
+#, c-format
+msgid "There is no enough balance to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:117
+#, c-format
+msgid "Send %1$s to your account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:121
+#, c-format
+msgid "There is no account to make a deposit for currency %1$s"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:127
+#, c-format
+msgid "Add account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:151
+#, c-format
+msgid "Select account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:163
+#, c-format
+msgid "Add another account"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:191
+#, c-format
+msgid "Deposit fee"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:205
+#, c-format
+msgid "Total deposit"
+msgstr ""
+
+#: src/wallet/DepositPage/views.tsx:233
+#, c-format
+msgid "Deposit&nbsp;%1$s %2$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:56
+#, c-format
+msgid "Add bank account for %1$s"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:59
+#, c-format
+msgid "Enter the URL of an exchange you trust."
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:66
+#, c-format
+msgid "Unable add this account"
+msgstr ""
+
+#: src/wallet/AddAccount/views.tsx:73
+#, c-format
+msgid "Select account type"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:42
+#, c-format
+msgid "Review terms of service"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:45
+#, c-format
+msgid "Exchange URL"
+msgstr ""
+
+#: src/wallet/ExchangeAddConfirm.tsx:70
+#, c-format
+msgid "Add exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:112
+#, c-format
+msgid "Add new exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:116
+#, c-format
+msgid "Add exchange for %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:128
+#, c-format
+msgid "An exchange has been found! Review the information and click next"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:135
+#, c-format
+msgid "This exchange doesn&apos;t match the expected currency %1$s"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:143
+#, c-format
+msgid "Unable to verify this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:151
+#, c-format
+msgid "Unable to add this exchange"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:167
+#, c-format
+msgid "loading"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:174
+#, c-format
+msgid "Version"
+msgstr ""
+
+#: src/wallet/ExchangeSetUrl.tsx:206
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:201
+#, c-format
+msgid "Waiting for confirmation"
+msgstr ""
+
+#: src/components/TransactionItem.tsx:266
+#, c-format
+msgid "PENDING"
+msgstr ""
+
+#: src/wallet/History.tsx:75
+#, c-format
+msgid "Could not load the list of transactions"
+msgstr ""
+
+#: src/wallet/History.tsx:233
+#, c-format
+msgid "Your transaction history is empty for this currency."
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:127
+#, c-format
+msgid "Add backup provider"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:131
+#, c-format
+msgid "Could not get provider information"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:140
+#, c-format
+msgid "Backup providers may charge for their service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:147
+#, c-format
+msgid "URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:158
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:212
+#, c-format
+msgid "Provider URL"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:218
+#, c-format
+msgid "Please review and accept this provider&apos;s terms of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:223
+#, c-format
+msgid "Pricing"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:226
+#, c-format
+msgid "free of charge"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:228
+#, c-format
+msgid "%1$s per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:235
+#, c-format
+msgid "Storage"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:238
+#, c-format
+msgid "%1$s megabytes of storage per year of service"
+msgstr ""
+
+#: src/wallet/ProviderAddPage.tsx:244
+#, c-format
+msgid "Accept terms of service"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:44
+#, c-format
+msgid "Could not parse the payto URI"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:45
+#, c-format
+msgid "Please check the uri"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:75
+#, c-format
+msgid "Exchange is ready for withdrawal"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:78
+#, c-format
+msgid "To complete the process you need to wire%1$s %2$s to the exchange bank account"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:87
+#, c-format
+msgid ""
+"Alternative, you can also scan this QR code or open %1$s if you have a banking "
+"app installed that supports RFC 8905"
+msgstr ""
+
+#: src/wallet/ReserveCreated.tsx:98
+#, c-format
+msgid "Cancel withdrawal"
+msgstr ""
+
+#: src/wallet/Settings.tsx:115
+#, c-format
+msgid "Could not toggle auto-open"
+msgstr ""
+
+#: src/wallet/Settings.tsx:121
+#, c-format
+msgid "Could not toggle clipboard"
+msgstr ""
+
+#: src/wallet/Settings.tsx:126
+#, c-format
+msgid "Navigator"
+msgstr ""
+
+#: src/wallet/Settings.tsx:129
+#, c-format
+msgid "Automatically open wallet based on page content"
+msgstr ""
+
+#: src/wallet/Settings.tsx:135
+#, c-format
+msgid ""
+"Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser."
+msgstr ""
+
+#: src/wallet/Settings.tsx:145
+#, c-format
+msgid "Automatically check clipboard for Taler URI"
+msgstr ""
+
+#: src/wallet/Settings.tsx:162
+#, c-format
+msgid "Trust"
+msgstr ""
+
+#: src/wallet/Settings.tsx:166
+#, c-format
+msgid "No exchange yet"
+msgstr ""
+
+#: src/wallet/Settings.tsx:180
+#, c-format
+msgid "Term of Service"
+msgstr ""
+
+#: src/wallet/Settings.tsx:191
+#, c-format
+msgid "ok"
+msgstr ""
+
+#: src/wallet/Settings.tsx:197
+#, c-format
+msgid "changed"
+msgstr ""
+
+#: src/wallet/Settings.tsx:204
+#, c-format
+msgid "not accepted"
+msgstr ""
+
+#: src/wallet/Settings.tsx:210
+#, c-format
+msgid "unknown (exchange status should be updated)"
+msgstr ""
+
+#: src/wallet/Settings.tsx:236
+#, c-format
+msgid "Add an exchange"
+msgstr ""
+
+#: src/wallet/Settings.tsx:241
+#, c-format
+msgid "Troubleshooting"
+msgstr ""
+
+#: src/wallet/Settings.tsx:244
+#, c-format
+msgid "Developer mode"
+msgstr ""
+
+#: src/wallet/Settings.tsx:246
+#, c-format
+msgid "More options and information useful for debugging"
+msgstr ""
+
+#: src/wallet/Settings.tsx:257
+#, c-format
+msgid "Display"
+msgstr ""
+
+#: src/wallet/Settings.tsx:261
+#, c-format
+msgid "Current Language"
+msgstr ""
+
+#: src/wallet/Settings.tsx:274
+#, c-format
+msgid "Wallet Core"
+msgstr ""
+
+#: src/wallet/Settings.tsx:284
+#, c-format
+msgid "Web Extension"
+msgstr ""
+
+#: src/wallet/Settings.tsx:295
+#, c-format
+msgid "Exchange compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:299
+#, c-format
+msgid "Merchant compatibility"
+msgstr ""
+
+#: src/wallet/Settings.tsx:303
+#, c-format
+msgid "Bank compatibility"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:59
+#, c-format
+msgid "Browser Extension Installed!"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:63
+#, c-format
+msgid "You can open the GNU Taler Wallet using the combination %1$s ."
+msgstr ""
+
+#: src/wallet/Welcome.tsx:72
+#, c-format
+msgid ""
+"Also pinning the GNU Taler Wallet to your Chrome browser allows you to quick "
+"access without keyboard:"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:79
+#, c-format
+msgid "Click the puzzle icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:82
+#, c-format
+msgid "Search for GNU Taler Wallet"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:85
+#, c-format
+msgid "Click the pin icon"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:91
+#, c-format
+msgid "Permissions"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:100
+#, c-format
+msgid ""
+"(Enabling this option below will make using the wallet faster, but requires more "
+"permissions from your browser.)"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:110
+#, c-format
+msgid "Next Steps"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:113
+#, c-format
+msgid "Try the demo"
+msgstr ""
+
+#: src/wallet/Welcome.tsx:116
+#, c-format
+msgid "Learn how to top up your wallet balance"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:31
+#, c-format
+msgid "Diagnostics timed out. Could not talk to the wallet backend."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:52
+#, c-format
+msgid "Problems detected:"
+msgstr ""
+
+#: src/components/Diagnostics.tsx:61
+#, c-format
+msgid ""
+"Please check in your %1$s settings that you have IndexedDB enabled (check the "
+"preference name %2$s)."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:70
+#, c-format
+msgid ""
+"Your wallet database is outdated. Currently automatic migration is not "
+"supported. Please go %1$s to reset the wallet database."
+msgstr ""
+
+#: src/components/Diagnostics.tsx:83
+#, c-format
+msgid "Running diagnostics"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:163
+#, c-format
+msgid "Debug tools"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:170
+#, c-format
+msgid ""
+"Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL "
+"YOUR COINS?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:176
+#, c-format
+msgid "reset"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:183
+#, c-format
+msgid "TESTING: This may delete all your coin, proceed with caution"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:189
+#, c-format
+msgid "run gc"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:197
+#, c-format
+msgid "import database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:219
+#, c-format
+msgid "export database"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:225
+#, c-format
+msgid "Database exported at %1$s %2$s to download"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:248
+#, c-format
+msgid "Coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:282
+#, c-format
+msgid "Pending operations"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:328
+#, c-format
+msgid "usable coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:337
+#, c-format
+msgid "id"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:340
+#, c-format
+msgid "denom"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:343
+#, c-format
+msgid "value"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:346
+#, c-format
+msgid "status"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:349
+#, c-format
+msgid "from refresh?"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:352
+#, c-format
+msgid "age key count"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:369
+#, c-format
+msgid "spent coins"
+msgstr ""
+
+#: src/wallet/DeveloperPage.tsx:373
+#, c-format
+msgid "click to show"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:108
+#, c-format
+msgid "Scan a QR code or enter taler:// URI below"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:122
+#, c-format
+msgid "Open"
+msgstr "Доступні"
+
+#: src/wallet/QrReader.tsx:128
+#, c-format
+msgid "URI is not valid. Taler URI should start with `taler://`"
+msgstr ""
+
+#: src/wallet/QrReader.tsx:133
+#, c-format
+msgid "Try another"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:183
+#, c-format
+msgid "Could not load list of exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:209
+#, c-format
+msgid "Choose a currency to proceed or add another exchange"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:217
+#, c-format
+msgid "Known currencies"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:318
+#, c-format
+msgid "Specify the amount and the origin"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:336
+#, c-format
+msgid "Change currency"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:344
+#, c-format
+msgid "Use previous origins:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:364
+#, c-format
+msgid "Or specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:372
+#, c-format
+msgid "Specify the origin of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:380
+#, c-format
+msgid "From my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:395
+#, c-format
+msgid "From another wallet"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:449
+#, c-format
+msgid "currency not provided"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:459
+#, c-format
+msgid "Specify the amount and the destination"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:483
+#, c-format
+msgid "Use previous destinations:"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:503
+#, c-format
+msgid "Or specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:511
+#, c-format
+msgid "Specify the destination of the money"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:521
+#, c-format
+msgid "To my bank account"
+msgstr ""
+
+#: src/wallet/DestinationSelection.tsx:534
+#, c-format
+msgid "To another wallet"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:30
+#, c-format
+msgid "Could not load backup recovery information"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:47
+#, c-format
+msgid "Digital wallet recovery"
+msgstr ""
+
+#: src/cta/Recovery/views.tsx:52
+#, c-format
+msgid "Import backup, show info"
+msgstr ""
+
+#: src/wallet/Application.tsx:189
+#, c-format
+msgid "All done, your transaction is in progress"
+msgstr ""
+
+#: src/components/EditableText.tsx:45
+#, c-format
+msgid "Edit"
+msgstr ""
+
+#: src/wallet/ManualWithdrawPage.tsx:102
+#, c-format
+msgid "Could not load the list of known exchanges"
+msgstr ""
diff --git a/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx b/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx
new file mode 100644
index 000000000..b0c2a2730
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Alert.stories.tsx
@@ -0,0 +1,107 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { css } from "@linaria/core";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { Alert } from "./Alert.jsx";
+
+export default {
+ title: "alert",
+ component: Alert,
+};
+
+function Wrapper({ children }: { children: ComponentChildren }): VNode {
+ return (
+ <div
+ class={css`
+ & > * {
+ margin: 2em;
+ }
+ `}
+ >
+ {children}
+ </div>
+ );
+}
+
+export const BasicExample = (): VNode => (
+ <Wrapper>
+ <Alert severity="warning">this is an warning</Alert>
+ <Alert severity="error">this is an error</Alert>
+ <Alert severity="success">this is an success</Alert>
+ <Alert severity="info">this is an info</Alert>
+ </Wrapper>
+);
+
+export const WithTitle = (): VNode => (
+ <Wrapper>
+ <Alert title={"Warning" as TranslatedString} severity="warning">
+ this is an warning
+ </Alert>
+ <Alert title={"Error" as TranslatedString} severity="error">
+ this is an error
+ </Alert>
+ <Alert title={"Success" as TranslatedString} severity="success">
+ this is an success
+ </Alert>
+ <Alert title={"Info" as TranslatedString} severity="info">
+ this is an info
+ </Alert>
+ </Wrapper>
+);
+
+const showSomething = async function (): Promise<void> {
+ alert("closed");
+};
+
+export const WithAction = (): VNode => (
+ <Wrapper>
+ <Alert
+ title={"Warning" as TranslatedString}
+ severity="warning"
+ onClose={showSomething}
+ >
+ this is an warning
+ </Alert>
+ <Alert
+ title={"Error" as TranslatedString}
+ severity="error"
+ onClose={showSomething}
+ >
+ this is an error
+ </Alert>
+ <Alert
+ title={"Success" as TranslatedString}
+ severity="success"
+ onClose={showSomething}
+ >
+ this is an success
+ </Alert>
+ <Alert
+ title={"Info" as TranslatedString}
+ severity="info"
+ onClose={showSomething}
+ >
+ this is an info
+ </Alert>
+ </Wrapper>
+);
diff --git a/packages/taler-wallet-webextension/src/mui/Alert.tsx b/packages/taler-wallet-webextension/src/mui/Alert.tsx
new file mode 100644
index 000000000..22ea0b8ab
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Alert.tsx
@@ -0,0 +1,175 @@
+/*
+ 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 { TranslatedString } from "@gnu-taler/taler-util";
+import { css } from "@linaria/core";
+import { ComponentChildren, h, VNode } from "preact";
+// eslint-disable-next-line import/extensions
+import CloseIcon from "../svg/close_24px.inline.svg";
+import ErrorOutlineIcon from "../svg/error_outline_outlined_24px.inline.svg";
+import InfoOutlinedIcon from "../svg/info_outlined_24px.inline.svg";
+import ReportProblemOutlinedIcon from "../svg/report_problem_outlined_24px.inline.svg";
+import SuccessOutlinedIcon from "../svg/success_outlined_24px.inline.svg";
+import { IconButton } from "./Button.js";
+import { darken, lighten } from "./colors/manipulation.js";
+import { Paper } from "./Paper.js";
+import { theme } from "./style.jsx";
+import { Typography } from "./Typography.js";
+
+const defaultIconMapping = {
+ success: SuccessOutlinedIcon,
+ warning: ReportProblemOutlinedIcon,
+ error: ErrorOutlineIcon,
+ info: InfoOutlinedIcon,
+};
+
+const baseStyle = css`
+ background-color: transparent;
+ display: flex;
+ padding: 6px 16px;
+`;
+
+const colorVariant = {
+ standard: css`
+ color: var(--color-light-06);
+ background-color: var(--color-background-light-09);
+ `,
+ outlined: css`
+ color: var(--color-light-06);
+ border-width: 1px;
+ border-style: solid;
+ border-color: var(--color-light);
+ `,
+ filled: css`
+ color: "#fff";
+ font-weight: ${theme.typography.fontWeightMedium};
+ background-color: var(--color-main);
+ `,
+};
+
+interface Props {
+ title?: TranslatedString;
+ variant?: "filled" | "outlined" | "standard";
+ role?: string;
+ onClose?: () => Promise<void>;
+ // icon: VNode;
+ severity?: "info" | "warning" | "success" | "error";
+ children: ComponentChildren;
+ icon?: boolean;
+}
+
+const getColor = theme.palette.mode === "light" ? darken : lighten;
+const getBackgroundColor = theme.palette.mode === "light" ? lighten : darken;
+
+function Icon({ svg }: { svg: VNode }): VNode {
+ return (
+ <div
+ class={css`
+ margin-right: 12px;
+ padding: 7px 0px;
+ display: flex;
+ font-size: 22px;
+ opacity: 0.9;
+ fill: currentColor;
+ `}
+ dangerouslySetInnerHTML={{ __html: svg as any }}
+ ></div>
+ );
+}
+
+function Action({ children }: { children: ComponentChildren }): VNode {
+ return (
+ <div
+ class={css`
+ display: flex;
+ align-items: flex-start;
+ padding: 4px 0px 0px 16px;
+ margin-left: auto;
+ margin-right: -8px;
+ `}
+ >
+ {children}
+ </div>
+ );
+}
+
+function Message({
+ title,
+ children,
+}: {
+ title?: TranslatedString;
+ children: ComponentChildren;
+}): VNode {
+ return (
+ <div
+ class={css`
+ padding: 8px 0px;
+ width: calc(100% - 48px - 36px);
+ `}
+ >
+ {title && (
+ <Typography
+ class={css`
+ font-weight: ${theme.typography.fontWeightBold};
+ `}
+ gutterBottom
+ >
+ {title}
+ </Typography>
+ )}
+ {children}
+ </div>
+ );
+}
+
+export function Alert({
+ variant = "standard",
+ severity = "success",
+ title,
+ children,
+ icon,
+ onClose,
+ ...rest
+}: Props): VNode {
+ return (
+ <Paper
+ class={[
+ theme.typography.body2,
+ baseStyle,
+ severity && colorVariant[variant],
+ ].join(" ")}
+ style={{
+ "--color-light-06": getColor(theme.palette[severity].light, 0.6),
+ "--color-background-light-09": getBackgroundColor(
+ theme.palette[severity].light,
+ 0.9,
+ ),
+ "--color-main": theme.palette[severity].main,
+ "--color-light": theme.palette[severity].light,
+ // ...(style as any),
+ textAlign: "left",
+ }}
+ elevation={1}
+ >
+ {icon != false ? <Icon svg={defaultIconMapping[severity]} /> : null}
+ <Message title={title}>{children}</Message>
+ {onClose && (
+ <Action>
+ <IconButton svg={CloseIcon} onClick={onClose} />
+ </Action>
+ )}
+ </Paper>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Avatar.tsx b/packages/taler-wallet-webextension/src/mui/Avatar.tsx
new file mode 100644
index 000000000..b6e37d2ce
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Avatar.tsx
@@ -0,0 +1,69 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { css } from "@linaria/core";
+import { h, JSX, VNode, ComponentChildren } from "preact";
+// eslint-disable-next-line import/extensions
+import { theme } from "./style.jsx";
+
+const root = css`
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 40px;
+ height: 40px;
+ font-family: ${theme.typography.fontFamily};
+ font-size: ${theme.typography.pxToRem(20)};
+ line-height: 1;
+ overflow: hidden;
+ user-select: none;
+`;
+
+// const colorStyle = css`
+// color: ${theme.palette.background.default};
+// background-color: ${theme.palette.mode === "light"
+// ? theme.palette.grey[400]
+// : theme.palette.grey[600]};
+// `;
+
+// const avatarImageStyle = css`
+// width: 100%;
+// height: 100%;
+// text-align: center;
+// object-fit: cover;
+// color: transparent;
+// text-indent: 10000;
+// `;
+
+interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
+ variant?: "circular" | "rounded" | "square";
+ children?: ComponentChildren;
+}
+
+export function Avatar({ variant, children, ...rest }: Props): VNode {
+ const borderStyle =
+ variant === "square"
+ ? theme.shape.squareBorder
+ : variant === "rounded"
+ ? theme.shape.roundBorder
+ : theme.shape.circularBorder;
+ return (
+ <div class={[root, borderStyle].join(" ")} {...rest}>
+ {children}
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Button.stories.tsx b/packages/taler-wallet-webextension/src/mui/Button.stories.tsx
new file mode 100644
index 000000000..5506caa42
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Button.stories.tsx
@@ -0,0 +1,163 @@
+/*
+ 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 { Button } from "./Button.js";
+import { Fragment, h, VNode } from "preact";
+import DeleteIcon from "../svg/delete_24px.inline.svg";
+import SendIcon from "../svg/send_24px.inline.svg";
+import { styled } from "@linaria/react";
+
+export default {
+ title: "Button",
+};
+
+const Stack = styled.div`
+ display: flex;
+ flex-direction: column;
+ & > button {
+ margin: 14px;
+ }
+ background-color: white;
+`;
+
+export const WithDelay = (): VNode => (
+ <Stack>
+ <Button
+ size="small"
+ variant="contained"
+ onClick={() =>
+ new Promise((resolve) => {
+ setTimeout(resolve, 2000);
+ })
+ }
+ >
+ Returns after 2 seconds
+ </Button>
+ <Button
+ size="small"
+ variant="contained"
+ onClick={() =>
+ new Promise((_, reject) => {
+ setTimeout(reject, 2000);
+ })
+ }
+ >
+ Fails after 2 seconds
+ </Button>
+ </Stack>
+);
+
+export const BasicExample = (): VNode => (
+ <Fragment>
+ <Stack>
+ <Button size="small" variant="text">
+ Text
+ </Button>
+ <Button size="small" variant="contained">
+ Contained
+ </Button>
+ <Button size="small" variant="outlined">
+ Outlined
+ </Button>
+ </Stack>
+ <Stack>
+ <Button variant="text">Text</Button>
+ <Button variant="contained">Contained</Button>
+ <Button variant="outlined">Outlined</Button>
+ </Stack>
+ <Stack>
+ <Button size="large" variant="text">
+ Text
+ </Button>
+ <Button size="large" variant="contained">
+ Contained
+ </Button>
+ <Button size="large" variant="outlined">
+ Outlined
+ </Button>
+ </Stack>
+ </Fragment>
+);
+
+export const Others = (): VNode => (
+ <Fragment>
+ <p>colors</p>
+ <Stack>
+ <Button color="secondary">Secondary</Button>
+ <Button variant="contained" color="success">
+ Success
+ </Button>
+ <Button variant="outlined" color="error">
+ Error
+ </Button>
+ </Stack>
+ <p>disabled</p>
+ <Stack>
+ <Button disabled variant="text">
+ Text
+ </Button>
+ <Button disabled variant="contained">
+ Contained
+ </Button>
+ <Button disabled variant="outlined">
+ Outlined
+ </Button>
+ </Stack>
+ </Fragment>
+);
+
+export const WithIcons = (): VNode => (
+ <Fragment>
+ <Stack>
+ <Button variant="outlined" size="small" startIcon={DeleteIcon}>
+ Delete
+ </Button>
+ <Button variant="contained" size="small" endIcon={SendIcon}>
+ Send
+ </Button>
+ <Button variant="text" size="small" endIcon={SendIcon}>
+ Send
+ </Button>
+ </Stack>
+ <Stack>
+ <Button variant="outlined" startIcon={DeleteIcon}>
+ Delete
+ </Button>
+ <Button variant="contained" endIcon={SendIcon}>
+ Send
+ </Button>
+ <Button variant="text" endIcon={SendIcon}>
+ Send
+ </Button>
+ </Stack>
+ <Stack>
+ <Button variant="outlined" size="large" startIcon={DeleteIcon}>
+ Delete
+ </Button>
+ <Button variant="contained" size="large" endIcon={SendIcon}>
+ Send
+ </Button>
+ <Button variant="text" size="large" endIcon={SendIcon}>
+ Send
+ </Button>
+ </Stack>
+ </Fragment>
+);
diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx
new file mode 100644
index 000000000..12a4d91ea
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Button.tsx
@@ -0,0 +1,409 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ComponentChildren, h, VNode, JSX } from "preact";
+import { css } from "@linaria/core";
+// eslint-disable-next-line import/extensions
+import {
+ theme,
+ Colors,
+ rippleEnabled,
+ rippleEnabledOutlined,
+} from "./style.js";
+// eslint-disable-next-line import/extensions
+import { alpha } from "./colors/manipulation.js";
+import { useState } from "preact/hooks";
+import { SafeHandler } from "./handlers.js";
+
+export const buttonBaseStyle = css`
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ box-sizing: border-box;
+ background-color: transparent;
+ outline: 0;
+ border: 0;
+ margin: 0;
+ border-radius: 0;
+ padding: 0;
+ cursor: pointer;
+ user-select: none;
+ vertical-align: middle;
+ text-decoration: none;
+ color: inherit;
+`;
+
+interface Props {
+ children?: ComponentChildren;
+ disabled?: boolean;
+ disableElevation?: boolean;
+ disableFocusRipple?: boolean;
+ endIcon?: string | VNode;
+ fullWidth?: boolean;
+ style?: h.JSX.CSSProperties;
+ href?: string;
+ size?: "small" | "medium" | "large";
+ startIcon?: VNode | string;
+ variant?: "contained" | "outlined" | "text";
+ tooltip?: string;
+ color?: Colors;
+ onClick?: () => Promise<void>;
+ // onClick?: SafeHandler<void>;
+}
+
+const button = css`
+ min-width: 64px;
+ &:hover {
+ text-decoration: none;
+ background-color: var(--text-primary-alpha-opacity);
+ @media (hover: none) {
+ background-color: transparent;
+ }
+ }
+ &:disabled {
+ color: ${theme.palette.action.disabled};
+ }
+`;
+const colorIconVariant = {
+ outlined: css`
+ fill: var(--color-main);
+ `,
+ contained: css`
+ fill: var(--color-contrastText);
+ `,
+ text: css`
+ fill: var(--color-main);
+ `,
+};
+
+const colorVariant = {
+ outlined: css`
+ color: var(--color-main);
+ border: 1px solid var(--color-main-alpha-half);
+ background-color: var(--color-contrastText);
+ &:hover {
+ border: 1px solid var(--color-main);
+ background-color: var(--color-main-alpha-opacity);
+ }
+ &:disabled {
+ border: 1px solid ${theme.palette.action.disabledBackground};
+ }
+ `,
+ contained: css`
+ color: var(--color-contrastText);
+ background-color: var(--color-main);
+ box-shadow: ${theme.shadows[2]};
+ &:hover {
+ background-color: var(--color-grey-or-dark);
+ }
+ &:active {
+ box-shadow: ${theme.shadows[8]};
+ }
+ &:focus-visible {
+ box-shadow: ${theme.shadows[6]};
+ }
+ &:disabled {
+ color: ${theme.palette.action.disabled};
+ box-shadow: ${theme.shadows[0]};
+ background-color: ${theme.palette.action.disabledBackground};
+ }
+ `,
+ text: css`
+ color: var(--color-main);
+ &:hover {
+ background-color: var(--color-main-alpha-opacity);
+ }
+ `,
+};
+
+const sizeIconVariant = {
+ outlined: {
+ small: css`
+ padding: 3px;
+ font-size: ${theme.pxToRem(7)};
+ `,
+ medium: css`
+ padding: 5px;
+ `,
+ large: css`
+ padding: 7px;
+ font-size: ${theme.pxToRem(10)};
+ `,
+ },
+ contained: {
+ small: css`
+ padding: 4px;
+ font-size: ${theme.pxToRem(13)};
+ `,
+ medium: css`
+ padding: 6px;
+ `,
+ large: css`
+ padding: 8px;
+ font-size: ${theme.pxToRem(10)};
+ `,
+ },
+ text: {
+ small: css`
+ padding: 4px;
+ font-size: ${theme.pxToRem(13)};
+ `,
+ medium: css`
+ padding: 6px;
+ `,
+ large: css`
+ padding: 8px;
+ font-size: ${theme.pxToRem(15)};
+ `,
+ },
+};
+const sizeVariant = {
+ outlined: {
+ small: css`
+ padding: 3px 9px;
+ font-size: ${theme.pxToRem(13)};
+ `,
+ medium: css`
+ padding: 5px 15px;
+ `,
+ large: css`
+ padding: 7px 21px;
+ font-size: ${theme.pxToRem(15)};
+ `,
+ },
+ contained: {
+ small: css`
+ padding: 4px 10px;
+ font-size: ${theme.pxToRem(13)};
+ `,
+ medium: css`
+ padding: 6px 16px;
+ `,
+ large: css`
+ padding: 8px 22px;
+ font-size: ${theme.pxToRem(15)};
+ `,
+ },
+ text: {
+ small: css`
+ padding: 4px 5px;
+ font-size: ${theme.pxToRem(13)};
+ `,
+ medium: css`
+ padding: 6px 8px;
+ `,
+ large: css`
+ padding: 8px 11px;
+ font-size: ${theme.pxToRem(15)};
+ `,
+ },
+};
+
+const fullWidthStyle = css`
+ width: 100%;
+`;
+
+export function Button({
+ children,
+ disabled,
+ startIcon: sip,
+ endIcon: eip,
+ fullWidth,
+ tooltip,
+ variant = "text",
+ size = "medium",
+ style: parentStyle,
+ color = "primary",
+ onClick: doClick,
+}: Props): VNode {
+ const style = css`
+ user-select: none;
+ width: 24px;
+ height: 24px;
+ display: inline-block;
+ fill: currentColor;
+ flex-shrink: 0;
+ transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+
+ & > svg {
+ font-size: 20;
+ }
+ `;
+
+ const startIcon = sip && (
+ <span
+ class={[
+ css`
+ margin-right: 8px;
+ margin-left: -4px;
+ mask: var(--image) no-repeat center;
+ `,
+ colorIconVariant[variant],
+ sizeIconVariant[variant][size],
+ style,
+ ].join(" ")}
+ //FIXME: check when sip can be a vnode
+ dangerouslySetInnerHTML={{ __html: sip as string }}
+ style={{
+ "--color-main": theme.palette[color].main,
+ "--color-contrastText": theme.palette[color].contrastText,
+ }}
+ />
+ );
+ const endIcon = eip && (
+ <span
+ class={[
+ css`
+ margin-right: -4px;
+ margin-left: 8px;
+ mask: var(--image) no-repeat center;
+ `,
+ colorIconVariant[variant],
+ sizeIconVariant[variant][size],
+ style,
+ ].join(" ")}
+ dangerouslySetInnerHTML={{ __html: eip as string }}
+ style={{
+ "--color-main": theme.palette[color].main,
+ "--color-contrastText": theme.palette[color].contrastText,
+ "--color-dark": theme.palette[color].dark,
+ }}
+ />
+ );
+ const [running, setRunning] = useState(false);
+
+ async function onClick(): Promise<void> {
+ if (!doClick || disabled || running) return;
+ setRunning(true);
+ try {
+ await doClick();
+ } finally {
+ setRunning(false);
+ }
+ }
+
+ return (
+ <ButtonBase
+ disabled={disabled || running || !doClick}
+ class={[
+ theme.typography.button,
+ theme.shape.roundBorder,
+ button,
+ fullWidth && fullWidthStyle,
+ colorVariant[variant],
+ sizeVariant[variant][size],
+ ].join(" ")}
+ containedRipple={variant === "contained"}
+ onClick={onClick}
+ style={{
+ ...parentStyle,
+ "--color-main": theme.palette[color].main,
+ "--color-contrastText": theme.palette[color].contrastText,
+ "--color-main-alpha-half": alpha(theme.palette[color].main, 0.5),
+ "--color-dark": theme.palette[color].dark,
+ "--color-light": theme.palette[color].light,
+ "--color-main-alpha-opacity": alpha(
+ theme.palette[color].main,
+ theme.palette.action.hoverOpacity,
+ ),
+ "--text-primary-alpha-opacity": alpha(
+ theme.palette.text.primary,
+ theme.palette.action.hoverOpacity,
+ ),
+ "--color-grey-or-dark": !color
+ ? theme.palette.grey.A100
+ : theme.palette[color].dark,
+ }}
+ title={tooltip}
+ >
+ {startIcon}
+ {children}
+ {endIcon}
+ </ButtonBase>
+ );
+}
+
+interface BaseProps extends JSX.HTMLAttributes<HTMLButtonElement> {
+ class: string;
+ onClick?: () => Promise<void>;
+ containedRipple?: boolean;
+ children?: ComponentChildren;
+ svg?: any;
+}
+
+function ButtonBase({
+ class: _class,
+ children,
+ containedRipple,
+ onClick,
+ svg,
+ ...rest
+}: BaseProps): VNode {
+ function doClick(): void {
+ if (onClick) onClick();
+ }
+ const classNames = [
+ buttonBaseStyle,
+ _class,
+ containedRipple ? rippleEnabled : rippleEnabledOutlined,
+ ].join(" ");
+ if (svg) {
+ return (
+ <button
+ onClick={doClick}
+ class={classNames}
+ dangerouslySetInnerHTML={{ __html: svg }}
+ {...rest}
+ />
+ );
+ }
+ return (
+ <button onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ doClick();
+ }} class={classNames} {...rest}>
+ {children}
+ </button>
+ );
+}
+
+export function IconButton({
+ svg,
+ onClick,
+}: {
+ svg: any;
+ onClick?: () => Promise<void>;
+}): VNode {
+ return (
+ <ButtonBase
+ onClick={onClick}
+ class={[
+ css`
+ text-align: center;
+ flex: 0 0 auto;
+ font-size: ${theme.typography.pxToRem(24)};
+ padding: 8px;
+ border-radius: 50%;
+ overflow: visible;
+ color: "inherit";
+ fill: currentColor;
+ `,
+ ].join(" ")}
+ svg={svg}
+ />
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Divider.tsx b/packages/taler-wallet-webextension/src/mui/Divider.tsx
new file mode 100644
index 000000000..6f5ae343e
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Divider.tsx
@@ -0,0 +1,20 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, Fragment, VNode } from "preact";
+
+export function Divider(): VNode {
+ return <Fragment />;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Grid.stories.tsx b/packages/taler-wallet-webextension/src/mui/Grid.stories.tsx
new file mode 100644
index 000000000..d399cb825
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Grid.stories.tsx
@@ -0,0 +1,212 @@
+/*
+ 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 { Grid } from "./Grid.js";
+import { Fragment, h, VNode } from "preact";
+
+export default {
+ title: "grid",
+ component: Grid,
+};
+
+function Item({ children }: any): VNode {
+ return (
+ <div
+ style={{
+ padding: 10,
+ backgroundColor: "white",
+ textAlign: "center",
+ color: "back",
+ }}
+ >
+ {children}
+ </div>
+ );
+}
+
+function Wrapper({ children }: any): VNode {
+ return (
+ <div
+ style={{
+ display: "flex",
+ backgroundColor: "lightgray",
+ padding: 10,
+ width: "100%",
+ // width: 400,
+ // height: 400,
+ justifyContent: "center",
+ }}
+ >
+ <div style={{ flexGrow: 1 }}>{children}</div>
+ </div>
+ );
+}
+
+export const BasicExample = (): VNode => (
+ <Fragment>
+ <Wrapper>
+ <Grid container spacing={2}>
+ <Grid item xs={8}>
+ <Item>xs=8</Item>
+ </Grid>
+ <Grid item xs={4}>
+ <Item>xs=4</Item>
+ </Grid>
+ <Grid item xs={4}>
+ <Item>xs=4</Item>
+ </Grid>
+ <Grid item xs={8}>
+ <Item>xs=8</Item>
+ </Grid>
+ </Grid>
+ </Wrapper>
+ <Wrapper>
+ <Grid container spacing={2}>
+ <Grid item xs={6} md={8}>
+ <Item>xs=6 md=8</Item>
+ </Grid>
+ <Grid item xs={6} md={4}>
+ <Item>xs=6 md=4</Item>
+ </Grid>
+ <Grid item xs={6} md={4}>
+ <Item>xs=6 md=4</Item>
+ </Grid>
+ <Grid item xs={6} md={8}>
+ <Item>xs=6 md=8</Item>
+ </Grid>
+ </Grid>
+ </Wrapper>
+ </Fragment>
+);
+
+export const Responsive12ColumnsSize = (): VNode => (
+ <Fragment>
+ <Wrapper>
+ <p>Item size is responsive: xs=6 sm=4 md=2</p>
+ <Grid container spacing={1} columns={12}>
+ {Array.from(Array(6)).map((_, index) => (
+ <Grid item xs={6} sm={4} md={2} key={index}>
+ <Item>item {index}</Item>
+ </Grid>
+ ))}
+ </Grid>
+ </Wrapper>
+ <Wrapper>
+ <p>Item size is fixed</p>
+ <Grid container spacing={1} columns={12}>
+ {Array.from(Array(6)).map((_, index) => (
+ <Grid item xs={6} key={index}>
+ <Item>item {index}</Item>
+ </Grid>
+ ))}
+ </Grid>
+ </Wrapper>
+ </Fragment>
+);
+
+export const Responsive12Spacing = (): VNode => (
+ <Fragment>
+ <Wrapper>
+ <p>Item space is responsive: xs=1 sm=2 md=3</p>
+ <Grid container spacing={{ xs: 2, sm: 4, md: 6 }} columns={12}>
+ {Array.from(Array(6)).map((_, index) => (
+ <Grid item xs={6} key={index}>
+ <Item>item {index}</Item>
+ </Grid>
+ ))}
+ </Grid>
+ </Wrapper>
+ <Wrapper>
+ <p>Item space is fixed</p>
+ <Grid container spacing={1} columns={12}>
+ {Array.from(Array(6)).map((_, index) => (
+ <Grid item xs={6} key={index}>
+ <Item>item {index}</Item>
+ </Grid>
+ ))}
+ </Grid>
+ </Wrapper>
+
+ <Wrapper>
+ <p>Item row space is responsive: xs=6 sm=4 md=1</p>
+ <Grid
+ container
+ rowSpacing={{ xs: 6, sm: 3, md: 1 }}
+ columnSpacing={1}
+ columns={12}
+ >
+ {Array.from(Array(6)).map((_, index) => (
+ <Grid item xs={6} key={index}>
+ <Item>item {index}</Item>
+ </Grid>
+ ))}
+ </Grid>
+ </Wrapper>
+ <Wrapper>
+ <p>Item col space is responsive: xs=6 sm=3 md=1</p>
+ <Grid
+ container
+ columnSpacing={{ xs: 6, sm: 3, md: 1 }}
+ rowSpacing={1}
+ columns={12}
+ >
+ {Array.from(Array(6)).map((_, index) => (
+ <Grid item xs={6} key={index}>
+ <Item>item {index}</Item>
+ </Grid>
+ ))}
+ </Grid>
+ </Wrapper>
+ </Fragment>
+);
+
+export const ResponsiveAuthWidth = (): VNode => (
+ <Fragment>
+ <Wrapper>
+ <Grid container columns={12}>
+ <Grid item>
+ <Item>item 1</Item>
+ </Grid>
+ <Grid item xs={1}>
+ <Item>item 2 short</Item>
+ </Grid>
+ <Grid item>
+ <Item>item 3 with long text </Item>
+ </Grid>
+ <Grid item xs={"true"}>
+ <Item>item 4</Item>
+ </Grid>
+ </Grid>
+ </Wrapper>
+ </Fragment>
+);
+export const Example = (): VNode => (
+ <Wrapper>
+ <p>Item row space is responsive: xs=6 sm=4 md=1</p>
+ <Grid container rowSpacing={3} columnSpacing={1} columns={12}>
+ {Array.from(Array(6)).map((_, index) => (
+ <Grid item xs={6} key={index}>
+ <Item>item {index}</Item>
+ </Grid>
+ ))}
+ </Grid>
+ </Wrapper>
+);
diff --git a/packages/taler-wallet-webextension/src/mui/Grid.tsx b/packages/taler-wallet-webextension/src/mui/Grid.tsx
new file mode 100644
index 000000000..2db439778
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Grid.tsx
@@ -0,0 +1,347 @@
+/*
+ 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 { css } from "@linaria/core";
+import { h, JSX, VNode, ComponentChildren, createContext } from "preact";
+import { useContext } from "preact/hooks";
+// eslint-disable-next-line import/extensions
+import { theme } from "./style.js";
+
+type ResponsiveKeys = "xs" | "sm" | "md" | "lg" | "xl";
+
+export type ResponsiveSize = {
+ xs: number;
+ sm: number;
+ md: number;
+ lg: number;
+ xl: number;
+};
+
+const root = css`
+ box-sizing: border-box;
+`;
+const containerStyle = css`
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+`;
+const itemStyle = css`
+ margin: 0;
+`;
+const zeroMinWidthStyle = css`
+ min-width: 0px;
+`;
+
+type GridSizes = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
+type SpacingSizes = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
+
+export interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
+ columns?: number | Partial<ResponsiveSize>;
+ container?: boolean;
+ item?: boolean;
+
+ direction?: "column-reverse" | "column" | "row-reverse" | "row";
+
+ lg?: GridSizes | "auto" | "true";
+ md?: GridSizes | "auto" | "true";
+ sm?: GridSizes | "auto" | "true";
+ xl?: GridSizes | "auto" | "true";
+ xs?: GridSizes | "auto" | "true";
+
+ wrap?: "nowrap" | "wrap-reverse" | "wrap";
+ spacing?: SpacingSizes | Partial<ResponsiveSize>;
+ columnSpacing?: SpacingSizes | Partial<ResponsiveSize>;
+ rowSpacing?: SpacingSizes | Partial<ResponsiveSize>;
+
+ alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline";
+ justifyContent?:
+ | "flex-start"
+ | "flex-end"
+ | "center"
+ | "space-around"
+ | "space-between"
+ | "space-evenly";
+
+ zeroMinWidth?: boolean;
+ children: ComponentChildren;
+}
+theme.breakpoints.up;
+
+function getOffset(val: number | string): string | number {
+ if (typeof val === "number") `${val}px`;
+ return val;
+}
+
+const columnGapVariant = css`
+ ${theme.breakpoints.up("xs")} {
+ width: calc(100% + var(--space-col-xs));
+ margin-left: calc(-1 * var(--space-col-xs));
+ & > div {
+ padding-left: var(--space-col-xs);
+ }
+ }
+ ${theme.breakpoints.up("sm")} {
+ width: calc(100% + var(--space-col-sm));
+ margin-left: calc(-1 * var(--space-col-sm));
+ & > div {
+ padding-left: var(--space-col-sm);
+ }
+ }
+ ${theme.breakpoints.up("md")} {
+ width: calc(100% + var(--space-col-md));
+ margin-left: calc(-1 * var(--space-col-md));
+ & > div {
+ padding-left: var(--space-col-md);
+ }
+ }
+ ${theme.breakpoints.up("lg")} {
+ width: calc(100% + var(--space-col-lg));
+ margin-left: calc(-1 * var(--space-col-lg));
+ & > div {
+ padding-left: var(--space-col-lg);
+ }
+ }
+ ${theme.breakpoints.up("xl")} {
+ width: calc(100% + var(--space-col-xl));
+ margin-left: calc(-1 * var(--space-col-xl));
+ & > div {
+ padding-left: var(--space-col-xl);
+ }
+ }
+`;
+const rowGapVariant = css`
+ ${theme.breakpoints.up("xs")} {
+ margin-top: calc(-1 * var(--space-row-xs));
+ & > div {
+ padding-top: var(--space-row-xs);
+ }
+ }
+ ${theme.breakpoints.up("sm")} {
+ margin-top: calc(-1 * var(--space-row-sm));
+ & > div {
+ padding-top: var(--space-row-sm);
+ }
+ }
+ ${theme.breakpoints.up("md")} {
+ margin-top: calc(-1 * var(--space-row-md));
+ & > div {
+ padding-top: var(--space-row-md);
+ }
+ }
+ ${theme.breakpoints.up("lg")} {
+ margin-top: calc(-1 * var(--space-row-lg));
+ & > div {
+ padding-top: var(--space-row-lg);
+ }
+ }
+ ${theme.breakpoints.up("xl")} {
+ margin-top: calc(-1 * var(--space-row-xl));
+ & > div {
+ padding-top: var(--space-row-xl);
+ }
+ }
+`;
+
+const sizeVariantXS = css`
+ ${theme.breakpoints.up("xs")} {
+ flex-basis: var(--relation-col-vs-xs);
+ flex-grow: 0;
+ max-width: var(--relation-col-vs-xs);
+ }
+`;
+const sizeVariantSM = css`
+ ${theme.breakpoints.up("sm")} {
+ flex-basis: var(--relation-col-vs-sm);
+ flex-grow: 0;
+ max-width: var(--relation-col-vs-sm);
+ }
+`;
+const sizeVariantMD = css`
+ ${theme.breakpoints.up("md")} {
+ flex-basis: var(--relation-col-vs-md);
+ flex-grow: 0;
+ max-width: var(--relation-col-vs-md);
+ }
+`;
+const sizeVariantLG = css`
+ ${theme.breakpoints.up("lg")} {
+ flex-basis: var(--relation-col-vs-lg);
+ flex-grow: 0;
+ max-width: var(--relation-col-vs-lg);
+ }
+`;
+const sizeVariantXL = css`
+ ${theme.breakpoints.up("xl")} {
+ flex-basis: var(--relation-col-vs-xl);
+ flex-grow: 0;
+ max-width: var(--relation-col-vs-xl);
+ }
+`;
+
+const sizeVariantExpand = css`
+ flex-basis: 0;
+ flex-grow: 1;
+ max-width: 100%;
+`;
+
+const sizeVariantAuto = css`
+ flex-basis: auto;
+ flex-grow: 0;
+ flex-shrink: 0;
+ max-width: none;
+ width: auto;
+`;
+
+const GridContext = createContext<Partial<ResponsiveSize>>(toResponsive(12));
+
+function toResponsive(
+ v: number | Partial<ResponsiveSize>,
+): Partial<ResponsiveSize> {
+ const p = typeof v === "number" ? { xs: v } : v;
+ const xs = p.xs;
+ const sm = p.sm || xs;
+ const md = p.md || sm;
+ const lg = p.lg || md;
+ const xl = p.xl || lg;
+ return {
+ xs,
+ sm,
+ md,
+ lg,
+ xl,
+ };
+}
+
+export function Grid({
+ columns: cp,
+ container = false,
+ item = false,
+ direction = "row",
+ lg,
+ md,
+ sm,
+ xl,
+ xs,
+ wrap = "wrap",
+ spacing = 0,
+ columnSpacing: csp,
+ rowSpacing: rsp,
+ alignItems,
+ justifyContent,
+ zeroMinWidth = false,
+ children,
+ onClick,
+ ...rest
+}: Props): VNode {
+ const cc = useContext(GridContext);
+ const columns = !cp ? cc : toResponsive(cp);
+
+ const rowSpacing = rsp ? toResponsive(rsp) : toResponsive(spacing);
+ const columnSpacing = csp ? toResponsive(csp) : toResponsive(spacing);
+
+ const ssize = toResponsive({ xs, md, lg, xl, sm } as any);
+
+ const spacingStyles = !container
+ ? {}
+ : {
+ "--space-col-xs": getOffset(theme.spacing(columnSpacing.xs)),
+ "--space-col-sm": getOffset(theme.spacing(columnSpacing.sm)),
+ "--space-col-md": getOffset(theme.spacing(columnSpacing.md)),
+ "--space-col-lg": getOffset(theme.spacing(columnSpacing.lg)),
+ "--space-col-xl": getOffset(theme.spacing(columnSpacing.xl)),
+
+ "--space-row-xs": getOffset(theme.spacing(rowSpacing.xs)),
+ "--space-row-sm": getOffset(theme.spacing(rowSpacing.sm)),
+ "--space-row-md": getOffset(theme.spacing(rowSpacing.md)),
+ "--space-row-lg": getOffset(theme.spacing(rowSpacing.lg)),
+ "--space-row-xl": getOffset(theme.spacing(rowSpacing.xl)),
+ };
+ const relationStyles = !item
+ ? {}
+ : {
+ "--relation-col-vs-xs": relation(columns, ssize, "xs"),
+ "--relation-col-vs-sm": relation(columns, ssize, "sm"),
+ "--relation-col-vs-md": relation(columns, ssize, "md"),
+ "--relation-col-vs-lg": relation(columns, ssize, "lg"),
+ "--relation-col-vs-xl": relation(columns, ssize, "xl"),
+ };
+
+ return (
+ <GridContext.Provider value={columns}>
+ <div
+ class={[
+ root,
+ container && containerStyle,
+ item && itemStyle,
+ zeroMinWidth && zeroMinWidthStyle,
+ xs &&
+ (xs === "auto"
+ ? sizeVariantAuto
+ : xs === "true"
+ ? sizeVariantExpand
+ : sizeVariantXS),
+ sm &&
+ (sm === "auto"
+ ? sizeVariantAuto
+ : sm === "true"
+ ? sizeVariantExpand
+ : sizeVariantSM),
+ md &&
+ (md === "auto"
+ ? sizeVariantAuto
+ : md === "true"
+ ? sizeVariantExpand
+ : sizeVariantMD),
+ lg &&
+ (lg === "auto"
+ ? sizeVariantAuto
+ : lg === "true"
+ ? sizeVariantExpand
+ : sizeVariantLG),
+ xl &&
+ (xl === "auto"
+ ? sizeVariantAuto
+ : xl === "true"
+ ? sizeVariantExpand
+ : sizeVariantXL),
+ container && columnGapVariant,
+ container && rowGapVariant,
+ ].join(" ")}
+ style={{
+ ...relationStyles,
+ ...spacingStyles,
+ justifyContent,
+ alignItems,
+ flexWrap: wrap,
+ cursor: onClick ? "pointer" : "inherit",
+ }}
+ onClick={onClick}
+ {...rest}
+ >
+ {children}
+ </div>
+ </GridContext.Provider>
+ );
+}
+function relation(
+ cols: Partial<ResponsiveSize>,
+ values: Partial<ResponsiveSize>,
+ size: ResponsiveKeys,
+): string {
+ const colsNum = typeof cols === "number" ? cols : cols[size] || 12;
+ return (
+ String(Math.round(((values[size] || 1) / colsNum) * 10e7) / 10e5) + "%"
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/InputFile.tsx b/packages/taler-wallet-webextension/src/mui/InputFile.tsx
new file mode 100644
index 000000000..40e9f9ace
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/InputFile.tsx
@@ -0,0 +1,78 @@
+/*
+ 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 { ComponentChildren, h, VNode } from "preact";
+import { useRef, useState } from "preact/hooks";
+import { Button } from "./Button.js";
+// import { MAX_IMAGE_SIZE as MAX_IMAGE_UPLOAD_SIZE } from "../../utils/constants";
+// import { InputProps, useField } from "./useField";
+
+const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
+
+interface Props {
+ children: ComponentChildren;
+ onChange: (v: string) => void;
+}
+
+export function InputFile<T>({ onChange, children }: Props): VNode {
+ const image = useRef<HTMLInputElement>(null);
+
+ const [sizeError, setSizeError] = useState(false);
+
+ return (
+ <div>
+ <p>
+ <Button
+ variant="contained"
+ onClick={async () => image.current?.click()}
+ >
+ {children}
+ </Button>
+ <input
+ ref={image}
+ style={{ display: "none" }}
+ type="file"
+ // name={String(name)}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return;
+ }
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true);
+ return;
+ }
+ setSizeError(false);
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return onChange(`data:${f[0].type};base64,${b64}` as any);
+ });
+ }}
+ />
+ </p>
+ {sizeError && <p>Image should be smaller than 1 MB</p>}
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx b/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx
new file mode 100644
index 000000000..200af8f57
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Menu.stories.tsx
@@ -0,0 +1,171 @@
+/*
+ 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 { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Menu, MenuItem } from "./Menu.jsx";
+import { Paper } from "./Paper.js";
+
+export default {
+ title: "menu",
+ component: Menu,
+};
+
+export const BasicExample = (): VNode => {
+ const [open, setOpen] = useState(false);
+ async function handleClose(): Promise<void> {
+ setOpen(false);
+ }
+ async function handleClick(): Promise<void> {
+ setOpen(true);
+ }
+ return (
+ <Menu open={open} onClose={handleClose} onClick={handleClick}>
+ <MenuItem onClick={handleClose}>Profile</MenuItem>
+ <MenuItem onClick={handleClose}>My account</MenuItem>
+ <MenuItem onClick={handleClose}>Logout</MenuItem>
+ </Menu>
+ );
+};
+
+import { styled } from "@linaria/react";
+import { theme } from "./style.js";
+import { Typography } from "./Typography.js";
+import { Divider } from "./Divider.js";
+
+const ListItemIcon = styled.div`
+ min-width: 36px;
+ color: ${theme.palette.action.active};
+ flex-shrink: 0;
+ display: inline-flex;
+`;
+
+const IconCut = (): VNode => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ focusable="false"
+ style={{
+ width: "1em",
+ height: "1em",
+ }}
+ >
+ <path d="M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3v-1L9.64 7.64zM6 8c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm0 12c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm6-7.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5.5.22.5.5-.22.5-.5.5zM19 3l-6 6 2 2 7-7V3z" />
+ </svg>
+);
+
+const IconCopy = (): VNode => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ focusable="false"
+ style={{
+ width: "1em",
+ height: "1em",
+ }}
+ >
+ <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
+ </svg>
+);
+
+const IconPaste = (): VNode => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ focusable="false"
+ style={{
+ width: "1em",
+ height: "1em",
+ }}
+ >
+ <path d="M19 2h-4.18C14.4.84 13.3 0 12 0c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm7 18H5V4h2v3h10V4h2v16z" />
+ </svg>
+);
+
+const IconCloud = (): VNode => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ focusable="false"
+ style={{
+ width: "1em",
+ height: "1em",
+ }}
+ >
+ <path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z" />
+ </svg>
+);
+
+const ListItemText = styled.div`
+ flex: 1 1 auto;
+ min-width: 0px;
+ margin-top: 4px;
+ margin-bottom: 4px;
+`;
+
+export function IconMenu(): VNode {
+ return (
+ <div style={{ width: 320 }}>
+ <Paper>
+ <ul>
+ <MenuItem>
+ <ListItemIcon>
+ <IconCut />
+ </ListItemIcon>
+ <ListItemText>Cut</ListItemText>
+ <Typography variant="body2" /* color="text.secondary" */>
+ ⌘X
+ </Typography>
+ </MenuItem>
+ <MenuItem>
+ <ListItemIcon>
+ <IconCopy />
+ </ListItemIcon>
+ <ListItemText>Copy</ListItemText>
+ <Typography variant="body2" /* color="text.secondary" */>
+ ⌘C
+ </Typography>
+ </MenuItem>
+ <MenuItem>
+ <ListItemIcon>
+ <IconPaste />
+ </ListItemIcon>
+ <ListItemText>Paste</ListItemText>
+ <Typography variant="body2" /* color="text.secondary" */>
+ ⌘V
+ </Typography>
+ </MenuItem>
+ <Divider />
+ <MenuItem>
+ <ListItemIcon>
+ <IconCloud />
+ </ListItemIcon>
+ <ListItemText>Web Clipboard</ListItemText>
+ </MenuItem>
+ </ul>
+ </Paper>
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Menu.tsx b/packages/taler-wallet-webextension/src/mui/Menu.tsx
new file mode 100644
index 000000000..dd8266931
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Menu.tsx
@@ -0,0 +1,135 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { css } from "@linaria/core";
+import { h, VNode, Fragment, ComponentChildren } from "preact";
+import { buttonBaseStyle } from "./Button.js";
+import { alpha } from "./colors/manipulation.js";
+import { Paper } from "./Paper.js";
+// eslint-disable-next-line import/extensions
+import { Colors, ripple, theme } from "./style.js";
+
+interface Props {
+ children: ComponentChildren;
+ onClose: () => Promise<void>;
+ onClick: () => Promise<void>;
+ open?: boolean;
+}
+
+const menuPaper = css`
+ max-height: calc(100% - 96px);
+ -webkit-overflow-scrolling: touch;
+`;
+
+const menuList = css`
+ outline: 0px;
+`;
+
+export function Menu({ children, onClose, onClick, open }: Props): VNode {
+ return (
+ <Popover class={menuPaper}>
+ <ul class={menuList}>{children}</ul>
+ </Popover>
+ );
+}
+
+const popoverRoot = css``;
+
+const popoverPaper = css`
+ position: absolute;
+ overflow-y: auto;
+ overflow-x: hidden;
+ min-width: 16px;
+ min-height: 16px;
+ max-width: calc(100% - 32px);
+ max-height: calc(100% - 32px);
+ outline: 0;
+`;
+
+function Popover({ children }: any): VNode {
+ return (
+ <div class={popoverRoot}>
+ <Paper class={popoverPaper}>{children}</Paper>
+ </div>
+ );
+}
+
+const root = css`
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ position: relative;
+ text-decoration: none;
+ min-height: 48px;
+ padding-top: 6px;
+ padding-bottom: 6px;
+ box-sizing: border-box;
+ white-space: nowrap;
+ appearance: none;
+
+ &:not([data-disableGutters]) {
+ padding-left: 16px;
+ padding-right: 16px;
+ }
+
+ [data-dividers] {
+ border-bottom: 1px solid ${theme.palette.divider};
+ background-clip: padding-box;
+ }
+ &:hover {
+ text-decoration: none;
+ background-color: var(--color-main-alpha-half);
+ @media (hover: none) {
+ background-color: transparent;
+ }
+ }
+`;
+
+export function MenuItem({
+ children,
+ onClick,
+ color = "primary",
+}: {
+ children: ComponentChildren;
+ onClick?: () => Promise<void>;
+ color?: Colors;
+}): VNode {
+ function doClick(): void {
+ // if (onClick) onClick();
+ return;
+ }
+ return (
+ <li
+ onClick={doClick}
+ disabled={false}
+ role="menuitem"
+ class={[buttonBaseStyle, root, ripple].join(" ")}
+ style={{
+ "--color-main": theme.palette[color].main,
+ "--color-dark": theme.palette[color].dark,
+ "--color-grey-or-dark": !color
+ ? theme.palette.grey.A100
+ : theme.palette[color].dark,
+ "--color-main-alpha-half": alpha(theme.palette[color].main, 0.7),
+ "--color-main-alpha-opacity": alpha(
+ theme.palette[color].main,
+ theme.palette.action.hoverOpacity,
+ ),
+ }}
+ >
+ {children}
+ </li>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Modal.tsx b/packages/taler-wallet-webextension/src/mui/Modal.tsx
new file mode 100644
index 000000000..0ea1372fa
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Modal.tsx
@@ -0,0 +1,152 @@
+/*
+ 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 { css } from "@linaria/core";
+import { h, JSX, VNode, ComponentChildren } from "preact";
+import { useCallback, useEffect, useRef, useState } from "preact/hooks";
+// eslint-disable-next-line import/extensions
+import { alpha } from "./colors/manipulation.js";
+import { ModalManager } from "./ModalManager.js";
+import { Portal } from "./Portal.js";
+// eslint-disable-next-line import/extensions
+import { theme } from "./style.js";
+
+const baseStyle = css`
+ position: fixed;
+ z-index: ${theme.zIndex.modal};
+ right: 0px;
+ bottom: 0px;
+ top: 0px;
+ left: 0px;
+`;
+
+interface Props {
+ class: string;
+ children: ComponentChildren;
+ open?: boolean;
+ exited?: boolean;
+ container?: VNode;
+}
+
+const defaultManager = new ModalManager();
+const manager = defaultManager;
+
+function getModal(): any {
+ return null; //TODO: fix
+}
+
+export function Modal({
+ open,
+ // exited,
+ class: _class,
+ children,
+
+ container,
+ ...rest
+}: Props): VNode {
+ const [exited, setExited] = useState(true);
+ const mountNodeRef = useRef<HTMLElement | undefined>(undefined);
+
+ const isTopModal = useCallback(
+ () => manager.isTopModal(getModal()),
+ [manager],
+ );
+
+ const handlePortalRef = useEventCallback<HTMLElement[], void>((node) => {
+ mountNodeRef.current = node;
+
+ if (!node) {
+ return;
+ }
+
+ // if (open && isTopModal()) {
+ // handleMounted();
+ // } else {
+ // ariaHidden(modalRef.current, true);
+ // }
+ });
+
+ return (
+ <Portal
+ // ref={mountNodeRef}
+ // container={container}
+ // disablePortal={disablePortal}
+ >
+ <div
+ class={[_class, baseStyle].join(" ")}
+ style={{
+ visibility: !open && exited ? "hidden" : "visible",
+ }}
+ >
+ {children}
+ </div>
+ </Portal>
+ );
+}
+
+function getOffsetTop(rect: any, vertical: any): number {
+ let offset = 0;
+
+ if (typeof vertical === "number") {
+ offset = vertical;
+ } else if (vertical === "center") {
+ offset = rect.height / 2;
+ } else if (vertical === "bottom") {
+ offset = rect.height;
+ }
+
+ return offset;
+}
+
+function getOffsetLeft(rect: any, horizontal: any): number {
+ let offset = 0;
+
+ if (typeof horizontal === "number") {
+ offset = horizontal;
+ } else if (horizontal === "center") {
+ offset = rect.width / 2;
+ } else if (horizontal === "right") {
+ offset = rect.width;
+ }
+
+ return offset;
+}
+
+function getTransformOriginValue(transformOrigin: any): string {
+ return [transformOrigin.horizontal, transformOrigin.vertical]
+ .map((n) => (typeof n === "number" ? `${n}px` : n))
+ .join(" ");
+}
+
+function resolveAnchorEl(anchorEl: any): any {
+ return typeof anchorEl === "function" ? anchorEl() : anchorEl;
+}
+
+function useEventCallback<Args extends unknown[], Return>(
+ fn: (...args: Args) => Return,
+): (...args: Args) => Return {
+ const ref = useRef(fn);
+ useEffect(() => {
+ ref.current = fn;
+ });
+ return useCallback(
+ (...args: Args) =>
+ // @ts-expect-error hide `this`
+ // tslint:disable-next-line:ban-comma-operator
+ (0, ref.current!)(...args),
+ [],
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/ModalManager.ts b/packages/taler-wallet-webextension/src/mui/ModalManager.ts
new file mode 100644
index 000000000..eee037467
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/ModalManager.ts
@@ -0,0 +1,328 @@
+/*
+ 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/>
+ */
+
+////////////////////
+function ownerDocument(node: Node | null | undefined): Document {
+ return (node && node.ownerDocument) || document;
+}
+function ownerWindow(node: Node | undefined): Window {
+ const doc = ownerDocument(node);
+ return doc.defaultView || window;
+}
+// A change of the browser zoom change the scrollbar size.
+// Credit https://github.com/twbs/bootstrap/blob/488fd8afc535ca3a6ad4dc581f5e89217b6a36ac/js/src/util/scrollbar.js#L14-L18
+function getScrollbarSize(doc: Document): number {
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes
+ const documentWidth = doc.documentElement.clientWidth;
+ return Math.abs(window.innerWidth - documentWidth);
+}
+
+/////////////////////
+
+export interface ManagedModalProps {
+ disableScrollLock?: boolean;
+}
+
+// Is a vertical scrollbar displayed?
+function isOverflowing(container: Element): boolean {
+ const doc = ownerDocument(container);
+
+ if (doc.body === container) {
+ return ownerWindow(container).innerWidth > doc.documentElement.clientWidth;
+ }
+
+ return container.scrollHeight > container.clientHeight;
+}
+
+export function ariaHidden(element: Element, show: boolean): void {
+ if (show) {
+ element.setAttribute("aria-hidden", "true");
+ } else {
+ element.removeAttribute("aria-hidden");
+ }
+}
+
+function getPaddingRight(element: Element): number {
+ return (
+ parseInt(ownerWindow(element).getComputedStyle(element).paddingRight, 10) ||
+ 0
+ );
+}
+
+function ariaHiddenSiblings(
+ container: Element,
+ mountElement: Element,
+ currentElement: Element,
+ elementsToExclude: readonly Element[] = [],
+ show: boolean,
+): void {
+ const blacklist = [mountElement, currentElement, ...elementsToExclude];
+ const blacklistTagNames = ["TEMPLATE", "SCRIPT", "STYLE"];
+
+ [].forEach.call(container.children, (element: Element) => {
+ if (
+ blacklist.indexOf(element) === -1 &&
+ blacklistTagNames.indexOf(element.tagName) === -1
+ ) {
+ ariaHidden(element, show);
+ }
+ });
+}
+
+function findIndexOf<T>(
+ items: readonly T[],
+ callback: (item: T) => boolean,
+): number {
+ let idx = -1;
+ items.some((item, index) => {
+ if (callback(item)) {
+ idx = index;
+ return true;
+ }
+ return false;
+ });
+ return idx;
+}
+
+function handleContainer(containerInfo: Container, props: ManagedModalProps) {
+ const restoreStyle: Array<{
+ /**
+ * CSS property name (HYPHEN CASE) to be modified.
+ */
+ property: string;
+ el: HTMLElement | SVGElement;
+ value: string;
+ }> = [];
+ const container = containerInfo.container;
+
+ if (!props.disableScrollLock) {
+ if (isOverflowing(container)) {
+ // Compute the size before applying overflow hidden to avoid any scroll jumps.
+ const scrollbarSize = getScrollbarSize(ownerDocument(container));
+
+ restoreStyle.push({
+ value: container.style.paddingRight,
+ property: "padding-right",
+ el: container,
+ });
+ // Use computed style, here to get the real padding to add our scrollbar width.
+ container.style.paddingRight = `${
+ getPaddingRight(container) + scrollbarSize
+ }px`;
+
+ // .mui-fixed is a global helper.
+ const fixedElements =
+ ownerDocument(container).querySelectorAll(".mui-fixed");
+ [].forEach.call(fixedElements, (element: HTMLElement | SVGElement) => {
+ restoreStyle.push({
+ value: element.style.paddingRight,
+ property: "padding-right",
+ el: element,
+ });
+ element.style.paddingRight = `${
+ getPaddingRight(element) + scrollbarSize
+ }px`;
+ });
+ }
+
+ // Improve Gatsby support
+ // https://css-tricks.com/snippets/css/force-vertical-scrollbar/
+ const parent = container.parentElement;
+ const containerWindow = ownerWindow(container);
+ const scrollContainer =
+ parent?.nodeName === "HTML" &&
+ containerWindow.getComputedStyle(parent).overflowY === "scroll"
+ ? parent
+ : container;
+
+ // Block the scroll even if no scrollbar is visible to account for mobile keyboard
+ // screensize shrink.
+ restoreStyle.push(
+ {
+ value: scrollContainer.style.overflow,
+ property: "overflow",
+ el: scrollContainer,
+ },
+ {
+ value: scrollContainer.style.overflowX,
+ property: "overflow-x",
+ el: scrollContainer,
+ },
+ {
+ value: scrollContainer.style.overflowY,
+ property: "overflow-y",
+ el: scrollContainer,
+ },
+ );
+
+ scrollContainer.style.overflow = "hidden";
+ }
+
+ const restore = () => {
+ restoreStyle.forEach(({ value, el, property }) => {
+ if (value) {
+ el.style.setProperty(property, value);
+ } else {
+ el.style.removeProperty(property);
+ }
+ });
+ };
+
+ return restore;
+}
+
+function getHiddenSiblings(container: Element) {
+ const hiddenSiblings: Element[] = [];
+ [].forEach.call(container.children, (element: Element) => {
+ if (element.getAttribute("aria-hidden") === "true") {
+ hiddenSiblings.push(element);
+ }
+ });
+ return hiddenSiblings;
+}
+
+interface Modal {
+ mount: Element;
+ modalRef: Element;
+}
+
+interface Container {
+ container: HTMLElement;
+ hiddenSiblings: Element[];
+ modals: Modal[];
+ restore: null | (() => void);
+}
+
+export class ModalManager {
+ private containers: Container[];
+
+ private modals: Modal[];
+
+ constructor() {
+ this.modals = [];
+ this.containers = [];
+ }
+
+ add(modal: Modal, container: HTMLElement): number {
+ let modalIndex = this.modals.indexOf(modal);
+ if (modalIndex !== -1) {
+ return modalIndex;
+ }
+
+ modalIndex = this.modals.length;
+ this.modals.push(modal);
+
+ // If the modal we are adding is already in the DOM.
+ if (modal.modalRef) {
+ ariaHidden(modal.modalRef, false);
+ }
+
+ const hiddenSiblings = getHiddenSiblings(container);
+ ariaHiddenSiblings(
+ container,
+ modal.mount,
+ modal.modalRef,
+ hiddenSiblings,
+ true,
+ );
+
+ const containerIndex = findIndexOf(
+ this.containers,
+ (item) => item.container === container,
+ );
+ if (containerIndex !== -1) {
+ this.containers[containerIndex].modals.push(modal);
+ return modalIndex;
+ }
+
+ this.containers.push({
+ modals: [modal],
+ container,
+ restore: null,
+ hiddenSiblings,
+ });
+
+ return modalIndex;
+ }
+
+ mount(modal: Modal, props: ManagedModalProps): void {
+ const containerIndex = findIndexOf(
+ this.containers,
+ (item) => item.modals.indexOf(modal) !== -1,
+ );
+ const containerInfo = this.containers[containerIndex];
+
+ if (!containerInfo.restore) {
+ containerInfo.restore = handleContainer(containerInfo, props);
+ }
+ }
+
+ remove(modal: Modal): number {
+ const modalIndex = this.modals.indexOf(modal);
+
+ if (modalIndex === -1) {
+ return modalIndex;
+ }
+
+ const containerIndex = findIndexOf(
+ this.containers,
+ (item) => item.modals.indexOf(modal) !== -1,
+ );
+ const containerInfo = this.containers[containerIndex];
+
+ containerInfo.modals.splice(containerInfo.modals.indexOf(modal), 1);
+ this.modals.splice(modalIndex, 1);
+
+ // If that was the last modal in a container, clean up the container.
+ if (containerInfo.modals.length === 0) {
+ // The modal might be closed before it had the chance to be mounted in the DOM.
+ if (containerInfo.restore) {
+ containerInfo.restore();
+ }
+
+ if (modal.modalRef) {
+ // In case the modal wasn't in the DOM yet.
+ ariaHidden(modal.modalRef, true);
+ }
+
+ ariaHiddenSiblings(
+ containerInfo.container,
+ modal.mount,
+ modal.modalRef,
+ containerInfo.hiddenSiblings,
+ false,
+ );
+ this.containers.splice(containerIndex, 1);
+ } else {
+ // Otherwise make sure the next top modal is visible to a screen reader.
+ const nextTop = containerInfo.modals[containerInfo.modals.length - 1];
+ // as soon as a modal is adding its modalRef is undefined. it can't set
+ // aria-hidden because the dom element doesn't exist either
+ // when modal was unmounted before modalRef gets null
+ if (nextTop.modalRef) {
+ ariaHidden(nextTop.modalRef, false);
+ }
+ }
+
+ return modalIndex;
+ }
+
+ isTopModal(modal: Modal): boolean {
+ return (
+ this.modals.length > 0 && this.modals[this.modals.length - 1] === modal
+ );
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Paper.stories.tsx b/packages/taler-wallet-webextension/src/mui/Paper.stories.tsx
new file mode 100644
index 000000000..b0e06d137
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Paper.stories.tsx
@@ -0,0 +1,148 @@
+/*
+ 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 { h, VNode } from "preact";
+import { Paper } from "./Paper.js";
+
+export default {
+ title: "paper",
+ component: Paper,
+};
+
+export const BasicExample = (): VNode => (
+ <div
+ style={{
+ display: "flex",
+ wrap: "nowrap",
+ backgroundColor: "lightgray",
+ width: "100%",
+ padding: 10,
+ justifyContent: "space-between",
+ }}
+ >
+ <Paper elevation={0}>
+ <div style={{ height: 128, width: 128 }} />
+ </Paper>
+ <Paper>
+ <div style={{ height: 128, width: 128 }} />
+ </Paper>
+ <Paper elevation={3}>
+ <div style={{ height: 128, width: 128 }} />
+ </Paper>
+ <Paper elevation={8}>
+ <div style={{ height: 128, width: 128 }} />
+ </Paper>
+ </div>
+);
+
+export const Outlined = (): VNode => (
+ <div
+ style={{
+ display: "flex",
+ wrap: "nowrap",
+ backgroundColor: "lightgray",
+ width: "100%",
+ padding: 10,
+ justifyContent: "space-around",
+ }}
+ >
+ <Paper variant="outlined">
+ <div
+ style={{
+ textAlign: "center",
+ height: 128,
+ width: 128,
+ lineHeight: "128px",
+ }}
+ >
+ round
+ </div>
+ </Paper>
+ <Paper variant="outlined" square>
+ <div
+ style={{
+ textAlign: "center",
+ height: 128,
+ width: 128,
+ lineHeight: "128px",
+ }}
+ >
+ square
+ </div>
+ </Paper>
+ </div>
+);
+
+export const Elevation = (): VNode => (
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "column",
+ backgroundColor: "lightgray",
+ width: "100%",
+ padding: 50,
+ justifyContent: "space-around",
+ }}
+ >
+ {[0, 1, 2, 3, 4, 6, 8, 12, 16, 24].map((elevation) => (
+ <div style={{ marginTop: 50 }} key={elevation}>
+ <Paper elevation={elevation}>
+ <div
+ style={{
+ textAlign: "center",
+ height: 60,
+ lineHeight: "60px",
+ }}
+ >{`elevation=${elevation}`}</div>
+ </Paper>
+ </div>
+ ))}
+ </div>
+);
+
+export const ElevationDark = (): VNode => (
+ <div
+ class="theme-dark"
+ style={{
+ display: "flex",
+ flexDirection: "column",
+ backgroundColor: "lightgray",
+ width: "100%",
+ padding: 50,
+ justifyContent: "space-around",
+ }}
+ >
+ to be implemented
+ {/* {[0, 1, 2, 3, 4, 6, 8, 12, 16, 24].map((elevation) => (
+ <div style={{ marginTop: 50 }} key={elevation}>
+ <Paper elevation={elevation}>
+ <div
+ style={{
+ textAlign: "center",
+ height: 60,
+ lineHeight: "60px",
+ }}
+ >{`elevation=${elevation}`}</div>
+ </Paper>
+ </div>
+ ))} */}
+ </div>
+);
diff --git a/packages/taler-wallet-webextension/src/mui/Paper.tsx b/packages/taler-wallet-webextension/src/mui/Paper.tsx
new file mode 100644
index 000000000..a44b1be0d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Paper.tsx
@@ -0,0 +1,85 @@
+/*
+ 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 { css } from "@linaria/core";
+import { h, JSX, VNode, ComponentChildren } from "preact";
+// eslint-disable-next-line import/extensions
+import { alpha } from "./colors/manipulation.js";
+// eslint-disable-next-line import/extensions
+import { theme } from "./style.js";
+
+const borderVariant = {
+ outlined: css`
+ border: 1px solid ${theme.palette.divider};
+ `,
+ elevation: css`
+ box-shadow: var(--theme-shadow-elevation);
+ `,
+};
+const baseStyle = css`
+ .theme-dark & {
+ background-image: var(--gradient-white-elevation);
+ }
+`;
+
+interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
+ elevation?: number;
+ square?: boolean;
+ variant?: "elevation" | "outlined";
+ children?: ComponentChildren;
+}
+
+export function Paper({
+ elevation = 1,
+ square,
+ variant = "elevation",
+ children,
+ class: _class,
+ style,
+ ...rest
+}: Props): VNode {
+ return (
+ <div
+ class={[
+ _class,
+ baseStyle,
+ !square && theme.shape.roundBorder,
+ borderVariant[variant],
+ ].join(" ")}
+ style={{
+ "--theme-shadow-elevation": theme.shadows[elevation],
+ "--gradient-white-elevation": `linear-gradient(${alpha(
+ "#fff",
+ getOverlayAlpha(elevation),
+ )}, ${alpha("#fff", getOverlayAlpha(elevation))})`,
+ ...(style as any),
+ }}
+ {...rest}
+ >
+ {children}
+ </div>
+ );
+}
+
+// Inspired by https://github.com/material-components/material-components-ios/blob/bca36107405594d5b7b16265a5b0ed698f85a5ee/components/Elevation/src/UIColor%2BMaterialElevation.m#L61
+function getOverlayAlpha(elevation: number): number {
+ let alphaValue;
+ if (elevation < 1) {
+ alphaValue = 5.11916 * elevation ** 2;
+ } else {
+ alphaValue = 4.5 * Math.log(elevation + 1) + 2;
+ }
+ return Number((alphaValue / 100).toFixed(2));
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Popover.tsx b/packages/taler-wallet-webextension/src/mui/Popover.tsx
new file mode 100644
index 000000000..da551c65d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Popover.tsx
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { css } from "@linaria/core";
+import { h, VNode, ComponentChildren } from "preact";
+
+const baseStyle = css``;
+
+interface Props {
+ class: string;
+ children: ComponentChildren;
+}
+
+export function Popover({ class: _class, children, ...rest }: Props): VNode {
+ return (
+ <div class={[_class, baseStyle].join(" ")} style={{}} {...rest}>
+ {children}
+ </div>
+ );
+}
+
+function getOffsetTop(rect: any, vertical: any): number {
+ let offset = 0;
+
+ if (typeof vertical === "number") {
+ offset = vertical;
+ } else if (vertical === "center") {
+ offset = rect.height / 2;
+ } else if (vertical === "bottom") {
+ offset = rect.height;
+ }
+
+ return offset;
+}
+
+function getOffsetLeft(rect: any, horizontal: any): number {
+ let offset = 0;
+
+ if (typeof horizontal === "number") {
+ offset = horizontal;
+ } else if (horizontal === "center") {
+ offset = rect.width / 2;
+ } else if (horizontal === "right") {
+ offset = rect.width;
+ }
+
+ return offset;
+}
+
+function getTransformOriginValue(transformOrigin: any): string {
+ return [transformOrigin.horizontal, transformOrigin.vertical]
+ .map((n) => (typeof n === "number" ? `${n}px` : n))
+ .join(" ");
+}
+
+function resolveAnchorEl(anchorEl: any): any {
+ return typeof anchorEl === "function" ? anchorEl() : anchorEl;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Portal.tsx b/packages/taler-wallet-webextension/src/mui/Portal.tsx
new file mode 100644
index 000000000..1d835abac
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Portal.tsx
@@ -0,0 +1,128 @@
+/*
+ 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 { css } from "@linaria/core";
+import { createPortal, forwardRef } from "preact/compat";
+import {
+ h,
+ JSX,
+ VNode,
+ ComponentChildren,
+ RefObject,
+ isValidElement,
+ cloneElement,
+ Fragment,
+} from "preact";
+import { Ref, useEffect, useState } from "preact/hooks";
+
+import { theme } from "./style.js";
+
+const baseStyle = css`
+ position: fixed;
+ z-index: ${theme.zIndex.modal};
+ right: 0px;
+ bottom: 0px;
+ top: 0px;
+ left: 0px;
+`;
+
+interface Props {
+ // class: string;
+ children: ComponentChildren;
+ disablePortal?: boolean;
+ container?: VNode;
+}
+
+export const Portal = forwardRef(function Portal(
+ { container, disablePortal, children }: Props,
+ ref: Ref<any>,
+): VNode {
+ const [mountNode, setMountNode] = useState<HTMLElement | undefined>(
+ undefined,
+ );
+ const handleRef = null;
+ // useForkRef(
+ // isValidElement(children) ? children.ref : null,
+ // ref,
+ // );
+
+ useEffect(() => {
+ if (!disablePortal) {
+ setMountNode(getContainer(container) || document.body);
+ }
+ }, [container, disablePortal]);
+
+ useEffect(() => {
+ if (mountNode && !disablePortal) {
+ setRef(ref, mountNode);
+ return () => {
+ setRef(ref, null);
+ };
+ }
+
+ return undefined;
+ }, [ref, mountNode, disablePortal]);
+
+ if (disablePortal) {
+ if (isValidElement(children)) {
+ return cloneElement(children, {
+ ref: handleRef,
+ });
+ }
+ return <Fragment>{children}</Fragment>;
+ }
+
+ return mountNode ? (
+ createPortal(<Fragment>{children}</Fragment>, mountNode)
+ ) : (
+ <Fragment />
+ );
+} as any);
+
+function getContainer(container: any): any {
+ return typeof container === "function" ? container() : container;
+}
+
+// function useForkRef<Instance>(
+// refA: React.Ref<Instance> | null | undefined,
+// refB: React.Ref<Instance> | null | undefined,
+// ): React.Ref<Instance> | null {
+// /**
+// * This will create a new function if the ref props change and are defined.
+// * This means react will call the old forkRef with `null` and the new forkRef
+// * with the ref. Cleanup naturally emerges from this behavior.
+// */
+// return useMemo(() => {
+// if (refA == null && refB == null) {
+// return null;
+// }
+// return (refValue) => {
+// setRef(refA, refValue);
+// setRef(refB, refValue);
+// };
+// }, [refA, refB]);
+// }
+
+function setRef<T>(
+ ref: RefObject<T | null> | ((instance: T | null) => void) | null | undefined,
+ value: T | null,
+): void {
+ if (typeof ref === "function") {
+ ref(value);
+ } else if (ref) {
+ ref.current = value;
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx b/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
new file mode 100644
index 000000000..1c41c2141
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
@@ -0,0 +1,154 @@
+/*
+ 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 { styled } from "@linaria/react";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { TextField, Props } from "./TextField.js";
+
+export default {
+ title: "TextField",
+ component: TextField,
+};
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ & > * {
+ margin-bottom: 20px !important;
+ }
+`;
+
+const Input = (variant: Props["variant"]): VNode => {
+ const [value, onChange] = useState("");
+ return (
+ <Container>
+ <TextField variant={variant} label="Name" {...{ value, onChange }} />
+ <TextField
+ variant={variant}
+ type="password"
+ label="Password"
+ {...{ value, onChange }}
+ />
+ <TextField
+ disabled
+ variant={variant}
+ label="Country"
+ helperText="this is disabled"
+ value="disabled"
+ />
+ <TextField
+ error={"Error"}
+ variant={variant}
+ label="Something"
+ {...{ value, onChange }}
+ />
+ <TextField
+ error={"Error"}
+ disabled
+ variant={variant}
+ label="Disabled and Error"
+ value="disabled with error"
+ helperText="this field has an error"
+ />
+ <TextField
+ variant={variant}
+ required
+ label="Name"
+ {...{ value, onChange }}
+ helperText="this field is required"
+ />
+ </Container>
+ );
+};
+
+export const InputStandard = (): VNode => Input("standard");
+export const InputFilled = (): VNode => Input("filled");
+
+export const Color = (): VNode => {
+ const [value, onChange] = useState("");
+ return (
+ <Container>
+ <TextField
+ variant="standard"
+ label="Outlined secondary"
+ color="secondary"
+ {...{ value, onChange }}
+ />
+ <TextField
+ label="Filled success"
+ variant="standard"
+ color="success"
+ {...{ value, onChange }}
+ />
+ <TextField
+ label="Standard warning"
+ variant="standard"
+ color="warning"
+ {...{ value, onChange }}
+ />
+ </Container>
+ );
+};
+
+const Multiline = (variant: Props["variant"]): VNode => {
+ const [value, onChange] = useState("");
+ return (
+ <Container>
+ <TextField
+ {...{ value, onChange }}
+ label="Multiline"
+ variant={variant}
+ multiline
+ maxRows={4}
+ />
+ <TextField
+ {...{ value, onChange }}
+ label="Max row 4"
+ variant={variant}
+ multiline
+ rows={10}
+ />
+ <TextField
+ {...{ value, onChange }}
+ label="Row 10"
+ variant={variant}
+ multiline
+ />
+ </Container>
+ );
+};
+export const MultilineStandard = (): VNode => Multiline("standard");
+export const MultilineFilled = (): VNode => Multiline("filled");
+
+export const Select = (): VNode => {
+ const [value, onChange] = useState("");
+ return (
+ <Container>
+ <TextField
+ {...{ value, onChange }}
+ label="select"
+ variant="standard"
+ select
+ />
+ </Container>
+ );
+};
diff --git a/packages/taler-wallet-webextension/src/mui/TextField.tsx b/packages/taler-wallet-webextension/src/mui/TextField.tsx
new file mode 100644
index 000000000..ab29fb78d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/TextField.tsx
@@ -0,0 +1,97 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { ComponentChildren, h, VNode } from "preact";
+import { FormControl } from "./input/FormControl.js";
+import { FormHelperText } from "./input/FormHelperText.js";
+import { InputFilled } from "./input/InputFilled.js";
+import { InputLabel } from "./input/InputLabel.js";
+import { InputStandard } from "./input/InputStandard.js";
+import { SelectFilled } from "./input/SelectFilled.js";
+import { SelectOutlined } from "./input/SelectOutlined.js";
+import { SelectStandard } from "./input/SelectStandard.js";
+// eslint-disable-next-line import/extensions
+import { Colors } from "./style.js";
+
+export interface Props {
+ autoComplete?: string;
+ autoFocus?: boolean;
+ color?: Colors;
+ disabled?: boolean;
+ error?: string | Error;
+ fullWidth?: boolean;
+ helperText?: VNode | string;
+ id?: string;
+ label?: VNode | string;
+ margin?: "dense" | "normal" | "none";
+ maxRows?: number;
+ minRows?: number;
+ multiline?: boolean;
+ onChange?: (s: string) => void;
+ onInput?: (s: string) => string;
+ inputmode?: string;
+ min?: string;
+ step?: string;
+ placeholder?: string;
+ required?: boolean;
+
+ startAdornment?: VNode;
+ endAdornment?: VNode;
+
+ //FIXME: change to "grabFocus"
+ // focused?: boolean;
+ rows?: number;
+ select?: boolean;
+ type?: string;
+ value?: string;
+ variant?: "filled" | "outlined" | "standard";
+ children?: ComponentChildren;
+}
+
+const inputVariant = {
+ standard: InputStandard,
+ filled: InputFilled,
+ outlined: InputStandard,
+};
+
+const selectVariant = {
+ standard: SelectStandard,
+ filled: SelectFilled,
+ outlined: SelectStandard,
+};
+
+export function TextField({
+ label,
+ select,
+ helperText,
+ children,
+ variant = "filled",
+ ...props
+}: Props): VNode {
+ // htmlFor={id} id={inputLabelId}
+ const Input = select ? selectVariant[variant] : inputVariant[variant];
+ return (
+ <FormControl {...props}>
+ {label && <InputLabel>{label}</InputLabel>}
+ <Input {...props}>{children}</Input>
+ {helperText && (
+ <FormHelperText error={props.error}>{helperText}</FormHelperText>
+ )}
+ {props.error ? (
+ <FormHelperText error="true">{props.error}</FormHelperText>
+ ) : undefined}
+ </FormControl>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Typography.tsx b/packages/taler-wallet-webextension/src/mui/Typography.tsx
new file mode 100644
index 000000000..c9c811c99
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Typography.tsx
@@ -0,0 +1,125 @@
+/*
+ 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 { css } from "@linaria/core";
+import { ComponentChildren, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+// eslint-disable-next-line import/extensions
+import { theme } from "./style.js";
+
+type VariantEnum =
+ | "body1"
+ | "body2"
+ | "button"
+ | "caption"
+ | "h1"
+ | "h2"
+ | "h3"
+ | "h4"
+ | "h5"
+ | "h6"
+ | "inherit"
+ | "overline"
+ | "subtitle1"
+ | "subtitle2";
+
+interface Props {
+ align?: "center" | "inherit" | "justify" | "left" | "right";
+ gutterBottom?: boolean;
+ bold?: boolean;
+ inline?: boolean;
+ noWrap?: boolean;
+ paragraph?: boolean;
+ variant?: VariantEnum;
+ children: string[] | string;
+ class?: string;
+}
+
+const defaultVariantMapping = {
+ h1: "h1",
+ h2: "h2",
+ h3: "h3",
+ h4: "h4",
+ h5: "h5",
+ h6: "h6",
+ subtitle1: "h6",
+ subtitle2: "h6",
+ body1: "p",
+ body2: "p",
+ inherit: "p",
+};
+
+const root = css`
+ margin: 0;
+`;
+
+const noWrapStyle = css`
+ overflow: "hidden";
+ text-overflow: "ellipsis";
+ white-space: "nowrap";
+`;
+const gutterBottomStyle = css`
+ margin-bottom: 0.35em;
+`;
+const paragraphStyle = css`
+ margin-bottom: 16px;
+`;
+const boldStyle = css`
+ font-weight: bold;
+`;
+
+export function Typography({
+ align,
+ gutterBottom = false,
+ noWrap = false,
+ paragraph = false,
+ variant = "body1",
+ bold,
+ inline,
+ children,
+ class: _class,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const Component = inline
+ ? "span"
+ : paragraph === true
+ ? "p"
+ : defaultVariantMapping[variant as "h1"] || "span";
+
+ const alignStyle =
+ align == "inherit"
+ ? {}
+ : {
+ textAlign: align,
+ };
+
+ return h(
+ Component,
+ {
+ class: [
+ _class,
+ root,
+ noWrap && noWrapStyle,
+ gutterBottom && gutterBottomStyle,
+ paragraph && paragraphStyle,
+ bold && boldStyle,
+ theme.typography[variant as "button"], //FIXME: implement the rest of the typography and remove the casting
+ ].join(" "),
+ style: alignStyle,
+ },
+ <i18n.Translate>{children}</i18n.Translate>,
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/colors/constants.ts b/packages/taler-wallet-webextension/src/mui/colors/constants.ts
new file mode 100644
index 000000000..0013d6cca
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/colors/constants.ts
@@ -0,0 +1,342 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+export const amber = {
+ 50: "#fff8e1",
+ 100: "#ffecb3",
+ 200: "#ffe082",
+ 300: "#ffd54f",
+ 400: "#ffca28",
+ 500: "#ffc107",
+ 600: "#ffb300",
+ 700: "#ffa000",
+ 800: "#ff8f00",
+ 900: "#ff6f00",
+ A100: "#ffe57f",
+ A200: "#ffd740",
+ A400: "#ffc400",
+ A700: "#ffab00",
+};
+
+export const blueGrey = {
+ 50: "#eceff1",
+ 100: "#cfd8dc",
+ 200: "#b0bec5",
+ 300: "#90a4ae",
+ 400: "#78909c",
+ 500: "#607d8b",
+ 600: "#546e7a",
+ 700: "#455a64",
+ 800: "#37474f",
+ 900: "#263238",
+ A100: "#cfd8dc",
+ A200: "#b0bec5",
+ A400: "#78909c",
+ A700: "#455a64",
+};
+
+export const blue = {
+ 50: "#e3f2fd",
+ 100: "#bbdefb",
+ 200: "#90caf9",
+ 300: "#64b5f6",
+ 400: "#42a5f5",
+ 500: "#2196f3",
+ 600: "#1e88e5",
+ 700: "#1976d2",
+ 800: "#1565c0",
+ 900: "#0d47a1",
+ A100: "#82b1ff",
+ A200: "#448aff",
+ A400: "#2979ff",
+ A700: "#2962ff",
+};
+
+export const brown = {
+ 50: "#efebe9",
+ 100: "#d7ccc8",
+ 200: "#bcaaa4",
+ 300: "#a1887f",
+ 400: "#8d6e63",
+ 500: "#795548",
+ 600: "#6d4c41",
+ 700: "#5d4037",
+ 800: "#4e342e",
+ 900: "#3e2723",
+ A100: "#d7ccc8",
+ A200: "#bcaaa4",
+ A400: "#8d6e63",
+ A700: "#5d4037",
+};
+
+export const common = {
+ black: "#000",
+ white: "#fff",
+};
+
+export const cyan = {
+ 50: "#e0f7fa",
+ 100: "#b2ebf2",
+ 200: "#80deea",
+ 300: "#4dd0e1",
+ 400: "#26c6da",
+ 500: "#00bcd4",
+ 600: "#00acc1",
+ 700: "#0097a7",
+ 800: "#00838f",
+ 900: "#006064",
+ A100: "#84ffff",
+ A200: "#18ffff",
+ A400: "#00e5ff",
+ A700: "#00b8d4",
+};
+
+export const deepOrange = {
+ 50: "#fbe9e7",
+ 100: "#ffccbc",
+ 200: "#ffab91",
+ 300: "#ff8a65",
+ 400: "#ff7043",
+ 500: "#ff5722",
+ 600: "#f4511e",
+ 700: "#e64a19",
+ 800: "#d84315",
+ 900: "#bf360c",
+ A100: "#ff9e80",
+ A200: "#ff6e40",
+ A400: "#ff3d00",
+ A700: "#dd2c00",
+};
+
+export const deepPurple = {
+ 50: "#ede7f6",
+ 100: "#d1c4e9",
+ 200: "#b39ddb",
+ 300: "#9575cd",
+ 400: "#7e57c2",
+ 500: "#673ab7",
+ 600: "#5e35b1",
+ 700: "#512da8",
+ 800: "#4527a0",
+ 900: "#311b92",
+ A100: "#b388ff",
+ A200: "#7c4dff",
+ A400: "#651fff",
+ A700: "#6200ea",
+};
+
+export const green = {
+ 50: "#e8f5e9",
+ 100: "#c8e6c9",
+ 200: "#a5d6a7",
+ 300: "#81c784",
+ 400: "#66bb6a",
+ 500: "#4caf50",
+ 600: "#43a047",
+ 700: "#388e3c",
+ 800: "#2e7d32",
+ 900: "#1b5e20",
+ A100: "#b9f6ca",
+ A200: "#69f0ae",
+ A400: "#00e676",
+ A700: "#00c853",
+};
+
+export const grey = {
+ 50: "#fafafa",
+ 100: "#f5f5f5",
+ 200: "#eeeeee",
+ 300: "#e0e0e0",
+ 400: "#bdbdbd",
+ 500: "#9e9e9e",
+ 600: "#757575",
+ 700: "#616161",
+ 800: "#424242",
+ 900: "#212121",
+ A100: "#f5f5f5",
+ A200: "#eeeeee",
+ A400: "#bdbdbd",
+ A700: "#616161",
+};
+
+export const indigo = {
+ 50: "#e8eaf6",
+ 100: "#c5cae9",
+ 200: "#9fa8da",
+ 300: "#7986cb",
+ 400: "#5c6bc0",
+ 500: "#3f51b5",
+ 600: "#3949ab",
+ 700: "#303f9f",
+ 800: "#283593",
+ 900: "#1a237e",
+ A100: "#8c9eff",
+ A200: "#536dfe",
+ A400: "#3d5afe",
+ A700: "#304ffe",
+};
+
+export const lightBlue = {
+ 50: "#e1f5fe",
+ 100: "#b3e5fc",
+ 200: "#81d4fa",
+ 300: "#4fc3f7",
+ 400: "#29b6f6",
+ 500: "#03a9f4",
+ 600: "#039be5",
+ 700: "#0288d1",
+ 800: "#0277bd",
+ 900: "#01579b",
+ A100: "#80d8ff",
+ A200: "#40c4ff",
+ A400: "#00b0ff",
+ A700: "#0091ea",
+};
+
+export const lightGreen = {
+ 50: "#f1f8e9",
+ 100: "#dcedc8",
+ 200: "#c5e1a5",
+ 300: "#aed581",
+ 400: "#9ccc65",
+ 500: "#8bc34a",
+ 600: "#7cb342",
+ 700: "#689f38",
+ 800: "#558b2f",
+ 900: "#33691e",
+ A100: "#ccff90",
+ A200: "#b2ff59",
+ A400: "#76ff03",
+ A700: "#64dd17",
+};
+
+export const lime = {
+ 50: "#f9fbe7",
+ 100: "#f0f4c3",
+ 200: "#e6ee9c",
+ 300: "#dce775",
+ 400: "#d4e157",
+ 500: "#cddc39",
+ 600: "#c0ca33",
+ 700: "#afb42b",
+ 800: "#9e9d24",
+ 900: "#827717",
+ A100: "#f4ff81",
+ A200: "#eeff41",
+ A400: "#c6ff00",
+ A700: "#aeea00",
+};
+
+export const orange = {
+ 50: "#fff3e0",
+ 100: "#ffe0b2",
+ 200: "#ffcc80",
+ 300: "#ffb74d",
+ 400: "#ffa726",
+ 500: "#ff9800",
+ 600: "#fb8c00",
+ 700: "#f57c00",
+ 800: "#ef6c00",
+ 900: "#e65100",
+ A100: "#ffd180",
+ A200: "#ffab40",
+ A400: "#ff9100",
+ A700: "#ff6d00",
+};
+
+export const pink = {
+ 50: "#fce4ec",
+ 100: "#f8bbd0",
+ 200: "#f48fb1",
+ 300: "#f06292",
+ 400: "#ec407a",
+ 500: "#e91e63",
+ 600: "#d81b60",
+ 700: "#c2185b",
+ 800: "#ad1457",
+ 900: "#880e4f",
+ A100: "#ff80ab",
+ A200: "#ff4081",
+ A400: "#f50057",
+ A700: "#c51162",
+};
+
+export const purple = {
+ 50: "#f3e5f5",
+ 100: "#e1bee7",
+ 200: "#ce93d8",
+ 300: "#ba68c8",
+ 400: "#ab47bc",
+ 500: "#9c27b0",
+ 600: "#8e24aa",
+ 700: "#7b1fa2",
+ 800: "#6a1b9a",
+ 900: "#4a148c",
+ A100: "#ea80fc",
+ A200: "#e040fb",
+ A400: "#d500f9",
+ A700: "#aa00ff",
+};
+
+export const red = {
+ 50: "#ffebee",
+ 100: "#ffcdd2",
+ 200: "#ef9a9a",
+ 300: "#e57373",
+ 400: "#ef5350",
+ 500: "#f44336",
+ 600: "#e53935",
+ 700: "#d32f2f",
+ 800: "#c62828",
+ 900: "#b71c1c",
+ A100: "#ff8a80",
+ A200: "#ff5252",
+ A400: "#ff1744",
+ A700: "#d50000",
+};
+
+export const teal = {
+ 50: "#e0f2f1",
+ 100: "#b2dfdb",
+ 200: "#80cbc4",
+ 300: "#4db6ac",
+ 400: "#26a69a",
+ 500: "#009688",
+ 600: "#00897b",
+ 700: "#00796b",
+ 800: "#00695c",
+ 900: "#004d40",
+ A100: "#a7ffeb",
+ A200: "#64ffda",
+ A400: "#1de9b6",
+ A700: "#00bfa5",
+};
+
+export const yellow = {
+ 50: "#fffde7",
+ 100: "#fff9c4",
+ 200: "#fff59d",
+ 300: "#fff176",
+ 400: "#ffee58",
+ 500: "#ffeb3b",
+ 600: "#fdd835",
+ 700: "#fbc02d",
+ 800: "#f9a825",
+ 900: "#f57f17",
+ A100: "#ffff8d",
+ A200: "#ffff00",
+ A400: "#ffea00",
+ A700: "#ffd600",
+};
diff --git a/packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts b/packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts
new file mode 100644
index 000000000..78e9d9cf7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts
@@ -0,0 +1,333 @@
+/*
+ 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 { expect } from "chai";
+import {
+ recomposeColor,
+ hexToRgb,
+ rgbToHex,
+ hslToRgb,
+ darken,
+ decomposeColor,
+ emphasize,
+ alpha,
+ getContrastRatio,
+ getLuminance,
+ lighten,
+} from "./manipulation.js";
+
+describe("utils/colorManipulator", () => {
+ describe("recomposeColor", () => {
+ it("converts a decomposed rgb color object to a string` ", () => {
+ expect(
+ recomposeColor({
+ type: "rgb",
+ values: [255, 255, 255],
+ }),
+ ).to.equal("rgb(255, 255, 255)");
+ });
+
+ it("converts a decomposed rgba color object to a string` ", () => {
+ expect(
+ recomposeColor({
+ type: "rgba",
+ values: [255, 255, 255, 0.5],
+ }),
+ ).to.equal("rgba(255, 255, 255, 0.5)");
+ });
+
+ it("converts a decomposed hsl color object to a string` ", () => {
+ expect(
+ recomposeColor({
+ type: "hsl",
+ values: [100, 50, 25],
+ }),
+ ).to.equal("hsl(100, 50%, 25%)");
+ });
+
+ it("converts a decomposed hsla color object to a string` ", () => {
+ expect(
+ recomposeColor({
+ type: "hsla",
+ values: [100, 50, 25, 0.5],
+ }),
+ ).to.equal("hsla(100, 50%, 25%, 0.5)");
+ });
+ });
+
+ describe("hexToRgb", () => {
+ it("converts a short hex color to an rgb color` ", () => {
+ expect(hexToRgb("#9f3")).to.equal("rgb(153, 255, 51)");
+ });
+
+ it("converts a long hex color to an rgb color` ", () => {
+ expect(hexToRgb("#a94fd3")).to.equal("rgb(169, 79, 211)");
+ });
+
+ it("converts a long alpha hex color to an argb color` ", () => {
+ expect(hexToRgb("#111111f8")).to.equal("rgba(17, 17, 17, 0.973)");
+ });
+ });
+
+ describe("rgbToHex", () => {
+ it("converts an rgb color to a hex color` ", () => {
+ expect(rgbToHex("rgb(169, 79, 211)")).to.equal("#a94fd3");
+ });
+
+ it("converts an rgba color to a hex color` ", () => {
+ expect(rgbToHex("rgba(169, 79, 211, 1)")).to.equal("#a94fd3ff");
+ });
+
+ it("idempotent", () => {
+ expect(rgbToHex("#A94FD3")).to.equal("#A94FD3");
+ });
+ });
+
+ describe("hslToRgb", () => {
+ it("converts an hsl color to an rgb color` ", () => {
+ expect(hslToRgb("hsl(281, 60%, 57%)")).to.equal("rgb(169, 80, 211)");
+ });
+
+ it("converts an hsla color to an rgba color` ", () => {
+ expect(hslToRgb("hsla(281, 60%, 57%, 0.5)")).to.equal(
+ "rgba(169, 80, 211, 0.5)",
+ );
+ });
+
+ it("allow to convert values only", () => {
+ expect(hslToRgb("hsl(281, 60%, 57%)")).to.equal("rgb(169, 80, 211)");
+ });
+ });
+
+ describe("decomposeColor", () => {
+ it("converts an rgb color string to an object with `type` and `value` keys", () => {
+ const { type, values } = decomposeColor("rgb(255, 255, 255)");
+ expect(type).to.equal("rgb");
+ expect(values).to.deep.equal([255, 255, 255]);
+ });
+
+ it("converts an rgba color string to an object with `type` and `value` keys", () => {
+ const { type, values } = decomposeColor("rgba(255, 255, 255, 0.5)");
+ expect(type).to.equal("rgba");
+ expect(values).to.deep.equal([255, 255, 255, 0.5]);
+ });
+
+ it("converts an hsl color string to an object with `type` and `value` keys", () => {
+ const { type, values } = decomposeColor("hsl(100, 50%, 25%)");
+ expect(type).to.equal("hsl");
+ expect(values).to.deep.equal([100, 50, 25]);
+ });
+
+ it("converts an hsla color string to an object with `type` and `value` keys", () => {
+ const { type, values } = decomposeColor("hsla(100, 50%, 25%, 0.5)");
+ expect(type).to.equal("hsla");
+ expect(values).to.deep.equal([100, 50, 25, 0.5]);
+ });
+
+ it("converts rgba hex", () => {
+ const decomposed = decomposeColor("#111111f8");
+ expect(decomposed).to.deep.equal({
+ type: "rgba",
+ colorSpace: undefined,
+ values: [17, 17, 17, 0.973],
+ });
+ });
+ });
+
+ describe("getContrastRatio", () => {
+ it("returns a ratio for black : white", () => {
+ expect(getContrastRatio("#000", "#FFF")).to.equal(21);
+ });
+
+ it("returns a ratio for black : black", () => {
+ expect(getContrastRatio("#000", "#000")).to.equal(1);
+ });
+
+ it("returns a ratio for white : white", () => {
+ expect(getContrastRatio("#FFF", "#FFF")).to.equal(1);
+ });
+
+ it("returns a ratio for dark-grey : light-grey", () => {
+ expect(getContrastRatio("#707070", "#E5E5E5")).to.be.approximately(
+ 3.93,
+ 0.01,
+ );
+ });
+
+ it("returns a ratio for black : light-grey", () => {
+ expect(getContrastRatio("#000", "#888")).to.be.approximately(5.92, 0.01);
+ });
+ });
+
+ describe("getLuminance", () => {
+ it("returns a valid luminance for rgb white ", () => {
+ expect(getLuminance("rgba(255, 255, 255)")).to.equal(1);
+ expect(getLuminance("rgb(255, 255, 255)")).to.equal(1);
+ });
+
+ it("returns a valid luminance for rgb mid-grey", () => {
+ expect(getLuminance("rgba(127, 127, 127)")).to.equal(0.212);
+ expect(getLuminance("rgb(127, 127, 127)")).to.equal(0.212);
+ });
+
+ it("returns a valid luminance for an rgb color", () => {
+ expect(getLuminance("rgb(255, 127, 0)")).to.equal(0.364);
+ });
+
+ it("returns a valid luminance from an hsl color", () => {
+ expect(getLuminance("hsl(100, 100%, 50%)")).to.equal(0.735);
+ });
+
+ it("returns an equal luminance for the same color in different formats", () => {
+ const hsl = "hsl(100, 100%, 50%)";
+ const rgb = "rgb(85, 255, 0)";
+ expect(getLuminance(hsl)).to.equal(getLuminance(rgb));
+ });
+ });
+
+ describe("emphasize", () => {
+ it("lightens a dark rgb color with the coefficient provided", () => {
+ expect(emphasize("rgb(1, 2, 3)", 0.4)).to.equal(
+ lighten("rgb(1, 2, 3)", 0.4),
+ );
+ });
+
+ it("darkens a light rgb color with the coefficient provided", () => {
+ expect(emphasize("rgb(250, 240, 230)", 0.3)).to.equal(
+ darken("rgb(250, 240, 230)", 0.3),
+ );
+ });
+
+ it("lightens a dark rgb color with the coefficient 0.15 by default", () => {
+ expect(emphasize("rgb(1, 2, 3)")).to.equal(lighten("rgb(1, 2, 3)", 0.15));
+ });
+
+ it("darkens a light rgb color with the coefficient 0.15 by default", () => {
+ expect(emphasize("rgb(250, 240, 230)")).to.equal(
+ darken("rgb(250, 240, 230)", 0.15),
+ );
+ });
+ });
+
+ describe("alpha", () => {
+ it("converts an rgb color to an rgba color with the value provided", () => {
+ expect(alpha("rgb(1, 2, 3)", 0.4)).to.equal("rgba(1, 2, 3, 0.4)");
+ });
+
+ it("updates an rgba color with the alpha value provided", () => {
+ expect(alpha("rgba(255, 0, 0, 0.2)", 0.5)).to.equal(
+ "rgba(255, 0, 0, 0.5)",
+ );
+ });
+
+ it("converts an hsl color to an hsla color with the value provided", () => {
+ expect(alpha("hsl(0, 100%, 50%)", 0.1)).to.equal(
+ "hsla(0, 100%, 50%, 0.1)",
+ );
+ });
+
+ it("updates an hsla color with the alpha value provided", () => {
+ expect(alpha("hsla(0, 100%, 50%, 0.2)", 0.5)).to.equal(
+ "hsla(0, 100%, 50%, 0.5)",
+ );
+ });
+ });
+
+ describe("darken", () => {
+ it("doesn't modify rgb black", () => {
+ expect(darken("rgb(0, 0, 0)", 0.1)).to.equal("rgb(0, 0, 0)");
+ });
+
+ it("darkens rgb white to black when coefficient is 1", () => {
+ expect(darken("rgb(255, 255, 255)", 1)).to.equal("rgb(0, 0, 0)");
+ });
+
+ it("retains the alpha value in an rgba color", () => {
+ expect(darken("rgba(0, 0, 0, 0.5)", 0.1)).to.equal("rgba(0, 0, 0, 0.5)");
+ });
+
+ it("darkens rgb white by 10% when coefficient is 0.1", () => {
+ expect(darken("rgb(255, 255, 255)", 0.1)).to.equal("rgb(229, 229, 229)");
+ });
+
+ it("darkens rgb red by 50% when coefficient is 0.5", () => {
+ expect(darken("rgb(255, 0, 0)", 0.5)).to.equal("rgb(127, 0, 0)");
+ });
+
+ it("darkens rgb grey by 50% when coefficient is 0.5", () => {
+ expect(darken("rgb(127, 127, 127)", 0.5)).to.equal("rgb(63, 63, 63)");
+ });
+
+ it("doesn't modify rgb colors when coefficient is 0", () => {
+ expect(darken("rgb(255, 255, 255)", 0)).to.equal("rgb(255, 255, 255)");
+ });
+
+ it("darkens hsl red by 50% when coefficient is 0.5", () => {
+ expect(darken("hsl(0, 100%, 50%)", 0.5)).to.equal("hsl(0, 100%, 25%)");
+ });
+
+ it("doesn't modify hsl colors when coefficient is 0", () => {
+ expect(darken("hsl(0, 100%, 50%)", 0)).to.equal("hsl(0, 100%, 50%)");
+ });
+
+ it("doesn't modify hsl colors when l is 0%", () => {
+ expect(darken("hsl(0, 50%, 0%)", 0.5)).to.equal("hsl(0, 50%, 0%)");
+ });
+ });
+
+ describe("lighten", () => {
+ it("doesn't modify rgb white", () => {
+ expect(lighten("rgb(255, 255, 255)", 0.1)).to.equal("rgb(255, 255, 255)");
+ });
+
+ it("lightens rgb black to white when coefficient is 1", () => {
+ expect(lighten("rgb(0, 0, 0)", 1)).to.equal("rgb(255, 255, 255)");
+ });
+
+ it("retains the alpha value in an rgba color", () => {
+ expect(lighten("rgba(255, 255, 255, 0.5)", 0.1)).to.equal(
+ "rgba(255, 255, 255, 0.5)",
+ );
+ });
+
+ it("lightens rgb black by 10% when coefficient is 0.1", () => {
+ expect(lighten("rgb(0, 0, 0)", 0.1)).to.equal("rgb(25, 25, 25)");
+ });
+
+ it("lightens rgb red by 50% when coefficient is 0.5", () => {
+ expect(lighten("rgb(255, 0, 0)", 0.5)).to.equal("rgb(255, 127, 127)");
+ });
+
+ it("lightens rgb grey by 50% when coefficient is 0.5", () => {
+ expect(lighten("rgb(127, 127, 127)", 0.5)).to.equal("rgb(191, 191, 191)");
+ });
+
+ it("doesn't modify rgb colors when coefficient is 0", () => {
+ expect(lighten("rgb(127, 127, 127)", 0)).to.equal("rgb(127, 127, 127)");
+ });
+
+ it("lightens hsl red by 50% when coefficient is 0.5", () => {
+ expect(lighten("hsl(0, 100%, 50%)", 0.5)).to.equal("hsl(0, 100%, 75%)");
+ });
+
+ it("doesn't modify hsl colors when coefficient is 0", () => {
+ expect(lighten("hsl(0, 100%, 50%)", 0)).to.equal("hsl(0, 100%, 50%)");
+ });
+
+ it("doesn't modify hsl colors when `l` is 100%", () => {
+ expect(lighten("hsl(0, 50%, 100%)", 0.5)).to.equal("hsl(0, 50%, 100%)");
+ });
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts b/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts
new file mode 100644
index 000000000..f9bf9eb2b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts
@@ -0,0 +1,328 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+export type ColorFormat = ColorFormatWithAlpha | ColorFormatWithoutAlpha;
+export type ColorFormatWithAlpha = "rgb" | "hsl";
+export type ColorFormatWithoutAlpha = "rgba" | "hsla";
+export type ColorObject = ColorObjectWithAlpha | ColorObjectWithoutAlpha;
+export interface ColorObjectWithAlpha {
+ type: ColorFormatWithAlpha;
+ values: [number, number, number];
+ colorSpace?: "srgb" | "display-p3" | "a98-rgb" | "prophoto-rgb" | "rec-2020";
+}
+export interface ColorObjectWithoutAlpha {
+ type: ColorFormatWithoutAlpha;
+ values: [number, number, number, number];
+ colorSpace?: "srgb" | "display-p3" | "a98-rgb" | "prophoto-rgb" | "rec-2020";
+}
+
+/**
+ * Returns a number whose value is limited to the given range.
+ * @param {number} value The value to be clamped
+ * @param {number} min The lower boundary of the output range
+ * @param {number} max The upper boundary of the output range
+ * @returns {number} A number in the range [min, max]
+ */
+function clamp(value: number, min = 0, max = 1): number {
+ // if (process.env.NODE_ENV !== 'production') {
+ // if (value < min || value > max) {
+ // console.error(`MUI: The value provided ${value} is out of range [${min}, ${max}].`);
+ // }
+ // }
+
+ return Math.min(Math.max(min, value), max);
+}
+
+/**
+ * Converts a color from CSS hex format to CSS rgb format.
+ * @param {string} color - Hex color, i.e. #nnn or #nnnnnn
+ * @returns {string} A CSS rgb color string
+ */
+export function hexToRgb(color: string): string {
+ color = color.substr(1);
+
+ const re = new RegExp(`.{1,${color.length >= 6 ? 2 : 1}}`, "g");
+ let colors = color.match(re);
+
+ if (colors && colors[0].length === 1) {
+ colors = colors.map((n) => n + n) as RegExpMatchArray;
+ }
+
+ return colors
+ ? `rgb${colors.length === 4 ? "a" : ""}(${colors
+ .map((n, index) => {
+ return index < 3
+ ? parseInt(n, 16)
+ : Math.round((parseInt(n, 16) / 255) * 1000) / 1000;
+ })
+ .join(", ")})`
+ : "";
+}
+
+function intToHex(int: number): string {
+ const hex = int.toString(16);
+ return hex.length === 1 ? `0${hex}` : hex;
+}
+
+/**
+ * Returns an object with the type and values of a color.
+ *
+ * Note: Does not support rgb % values.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()
+ * @returns {object} - A MUI color object: {type: string, values: number[]}
+ */
+export function decomposeColor(color: string): ColorObject {
+ const colorSpace = undefined;
+ if (color.charAt(0) === "#") {
+ return decomposeColor(hexToRgb(color));
+ }
+
+ const marker = color.indexOf("(");
+ const type = color.substring(0, marker);
+ // if (type != 'rgba' && type != 'hsla' && type != 'rgb' && type != 'hsl') {
+ // }
+
+ const values = color.substring(marker + 1, color.length - 1).split(",");
+ if (type == "rgb" || type == "hsl") {
+ return {
+ type,
+ colorSpace,
+ values: [
+ parseFloat(values[0]),
+ parseFloat(values[1]),
+ parseFloat(values[2]),
+ ],
+ };
+ }
+ if (type == "rgba" || type == "hsla") {
+ return {
+ type,
+ colorSpace,
+ values: [
+ parseFloat(values[0]),
+ parseFloat(values[1]),
+ parseFloat(values[2]),
+ parseFloat(values[3]),
+ ],
+ };
+ }
+ throw new Error(
+ `Unsupported '${color}' color. The following formats are supported: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()`,
+ );
+}
+
+/**
+ * Converts a color object with type and values to a string.
+ * @param {object} color - Decomposed color
+ * @param {string} color.type - One of: 'rgb', 'rgba', 'hsl', 'hsla'
+ * @param {array} color.values - [n,n,n] or [n,n,n,n]
+ * @returns {string} A CSS color string
+ */
+export function recomposeColor(color: ColorObject): string {
+ const { type, values: valuesNum } = color;
+
+ const valuesStr: string[] = [];
+ if (type.indexOf("rgb") !== -1) {
+ // Only convert the first 3 values to int (i.e. not alpha)
+ valuesNum
+ .map((n, i) => (i < 3 ? parseInt(String(n), 10) : n))
+ .forEach((n, i) => (valuesStr[i] = String(n)));
+ } else if (type.indexOf("hsl") !== -1) {
+ valuesStr[0] = String(valuesNum[0]);
+ valuesStr[1] = `${valuesNum[1]}%`;
+ valuesStr[2] = `${valuesNum[2]}%`;
+ if (type === "hsla") {
+ valuesStr[3] = String(valuesNum[3]);
+ }
+ }
+
+ return `${type}(${valuesStr.join(", ")})`;
+}
+
+/**
+ * Converts a color from CSS rgb format to CSS hex format.
+ * @param {string} color - RGB color, i.e. rgb(n, n, n)
+ * @returns {string} A CSS rgb color string, i.e. #nnnnnn
+ */
+export function rgbToHex(color: string): string {
+ // Idempotent
+ if (color.indexOf("#") === 0) {
+ return color;
+ }
+
+ const { values } = decomposeColor(color);
+ return `#${values
+ .map((n, i) => intToHex(i === 3 ? Math.round(255 * n) : n))
+ .join("")}`;
+}
+
+/**
+ * Converts a color from hsl format to rgb format.
+ * @param {string} color - HSL color values
+ * @returns {string} rgb color values
+ */
+export function hslToRgb(color: string): string {
+ const colorObj = decomposeColor(color);
+ const { values } = colorObj;
+ const h = values[0];
+ const s = values[1] / 100;
+ const l = values[2] / 100;
+ const a = s * Math.min(l, 1 - l);
+ const f = (n: number, k = (n + h / 30) % 12): number =>
+ l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
+
+ if (colorObj.type === "hsla") {
+ return recomposeColor({
+ type: "rgba",
+ values: [
+ Math.round(f(0) * 255),
+ Math.round(f(8) * 255),
+ Math.round(f(4) * 255),
+ colorObj.values[3],
+ ],
+ });
+ }
+
+ return recomposeColor({
+ type: "rgb",
+ values: [
+ Math.round(f(0) * 255),
+ Math.round(f(8) * 255),
+ Math.round(f(4) * 255),
+ ],
+ });
+}
+/**
+ * The relative brightness of any point in a color space,
+ * normalized to 0 for darkest black and 1 for lightest white.
+ *
+ * Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
+ * @returns {number} The relative brightness of the color in the range 0 - 1
+ */
+export function getLuminance(color: string): number {
+ const colorObj = decomposeColor(color);
+
+ const rgb2 =
+ colorObj.type === "hsl"
+ ? decomposeColor(hslToRgb(color)).values
+ : colorObj.values;
+ const rgb = rgb2.map((val) => {
+ val /= 255; // normalized
+ return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
+ }) as typeof rgb2;
+
+ // Truncate at 3 digits
+ return Number(
+ (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]).toFixed(3),
+ );
+}
+
+/**
+ * Calculates the contrast ratio between two colors.
+ *
+ * Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
+ * @param {string} foreground - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()
+ * @param {string} background - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()
+ * @returns {number} A contrast ratio value in the range 0 - 21.
+ */
+export function getContrastRatio(
+ foreground: string,
+ background: string,
+): number {
+ const lumA = getLuminance(foreground);
+ const lumB = getLuminance(background);
+ return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05);
+}
+
+/**
+ * Sets the absolute transparency of a color.
+ * Any existing alpha values are overwritten.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
+ * @param {number} value - value to set the alpha channel to in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function alpha(color: string, value: number): string {
+ const colorObj = decomposeColor(color);
+ value = clamp(value);
+
+ if (colorObj.type === "rgb" || colorObj.type === "hsl") {
+ colorObj.type += "a";
+ }
+ colorObj.values[3] = value;
+
+ return recomposeColor(colorObj);
+}
+
+/**
+ * Darkens a color.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
+ * @param {number} coefficient - multiplier in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function darken(color: string, coefficient: number): string {
+ const colorObj = decomposeColor(color);
+ coefficient = clamp(coefficient);
+
+ if (colorObj.type.indexOf("hsl") !== -1) {
+ colorObj.values[2] *= 1 - coefficient;
+ } else if (
+ colorObj.type.indexOf("rgb") !== -1 ||
+ colorObj.type.indexOf("color") !== -1
+ ) {
+ for (let i = 0; i < 3; i += 1) {
+ colorObj.values[i] *= 1 - coefficient;
+ }
+ }
+ return recomposeColor(colorObj);
+}
+
+/**
+ * Lightens a color.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
+ * @param {number} coefficient - multiplier in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function lighten(color: string, coefficient: number): string {
+ const colorObj = decomposeColor(color);
+ coefficient = clamp(coefficient);
+
+ if (colorObj.type.indexOf("hsl") !== -1) {
+ colorObj.values[2] += (100 - colorObj.values[2]) * coefficient;
+ } else if (colorObj.type.indexOf("rgb") !== -1) {
+ for (let i = 0; i < 3; i += 1) {
+ colorObj.values[i] += (255 - colorObj.values[i]) * coefficient;
+ }
+ } else if (colorObj.type.indexOf("color") !== -1) {
+ for (let i = 0; i < 3; i += 1) {
+ colorObj.values[i] += (1 - colorObj.values[i]) * coefficient;
+ }
+ }
+
+ return recomposeColor(colorObj);
+}
+
+/**
+ * Darken or lighten a color, depending on its luminance.
+ * Light colors are darkened, dark colors are lightened.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
+ * @param {number} coefficient=0.15 - multiplier in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function emphasize(color: string, coefficient = 0.15): string {
+ return getLuminance(color) > 0.5
+ ? darken(color, coefficient)
+ : lighten(color, coefficient);
+}
diff --git a/packages/taler-wallet-webextension/src/mui/handlers.ts b/packages/taler-wallet-webextension/src/mui/handlers.ts
new file mode 100644
index 000000000..a194bd02a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/handlers.ts
@@ -0,0 +1,82 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { AmountJson } from "@gnu-taler/taler-util";
+
+export interface TextFieldHandler {
+ onInput?: SafeHandler<string>;
+ value: string;
+ error?: string | Error;
+}
+
+export interface AmountFieldHandler {
+ onInput?: SafeHandler<AmountJson>;
+ value: AmountJson;
+ error?: string | Error;
+}
+
+declare const __safe_handler: unique symbol;
+export type SafeHandler<T> = {
+ <Req extends T>(req: Req): Promise<void>;
+ (): Promise<void>;
+ [__safe_handler]: true;
+};
+
+type UnsafeHandler<T> = ((p: T) => Promise<void>) | ((p: T) => void);
+
+export function withSafe<T>(
+ handler: UnsafeHandler<T>,
+ onError: (e: Error) => void,
+): SafeHandler<T> {
+ const sh = async function (p: T): Promise<void> {
+ try {
+ await handler(p);
+ } catch (e) {
+ if (e instanceof Error) {
+ onError(e);
+ } else {
+ onError(new Error(String(e)));
+ }
+ }
+ };
+ return sh as SafeHandler<T>;
+}
+
+export const nullFunction = async function (): Promise<void> {
+ //do nothing
+} as SafeHandler<void>;
+
+//FIXME: UI button should required SafeHandler but
+//useStateComponent should not be required to create SafeHandlers
+//so this need to be split in two:
+// * ButtonHandlerUI => with i18n
+// * ButtonHandlerLogic => without i18n
+export interface ButtonHandler {
+ onClick?: SafeHandler<void>;
+ // error?: TalerError;
+}
+
+export interface ToggleHandler {
+ value?: boolean;
+ button: ButtonHandler;
+}
+
+export interface SelectFieldHandler {
+ onChange?: SafeHandler<string>;
+ error?: string;
+ value: string;
+ isDirty?: boolean;
+ list: Record<string, string>;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/index.stories.tsx b/packages/taler-wallet-webextension/src/mui/index.stories.tsx
new file mode 100644
index 000000000..aa8dd2526
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/index.stories.tsx
@@ -0,0 +1,27 @@
+/*
+ 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)
+ */
+
+export * as a1 from "./Button.stories.js";
+export * as a3 from "./Grid.stories.js";
+export * as a4 from "./Paper.stories.js";
+export * as a5 from "./TextField.stories.js";
+export * as a6 from "./Alert.stories.js";
+export * as a7 from "./Menu.stories.js";
diff --git a/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx b/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx
new file mode 100644
index 000000000..45f5a81d1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx
@@ -0,0 +1,176 @@
+/*
+ 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 { css } from "@linaria/core";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext, useMemo, useState } from "preact/hooks";
+// eslint-disable-next-line import/extensions
+import { Colors } from "../style.js";
+
+export interface Props {
+ color: Colors;
+ disabled: boolean;
+ error?: string | Error;
+ focused: boolean;
+ fullWidth: boolean;
+ hiddenLabel: boolean;
+ required: boolean;
+ variant: "filled" | "outlined" | "standard";
+ margin: "none" | "normal" | "dense";
+ size: "medium" | "small";
+ children: ComponentChildren;
+}
+
+export const root = css`
+ display: inline-flex;
+ flex-direction: column;
+ position: relative;
+ min-width: 0px;
+ padding: 0px;
+ margin: 0px;
+ border: 0px;
+ vertical-align: top;
+`;
+
+const marginVariant = {
+ none: "",
+ normal: css`
+ margin-top: 16px;
+ margin-bottom: 8px;
+ `,
+ dense: css`
+ margin-top: 8px;
+ margin-bottom: 4px;
+ `,
+};
+const fullWidthStyle = css`
+ width: 100%;
+`;
+
+export const FormControlContext = createContext<FCCProps | null>(null);
+
+export function FormControl({
+ color = "primary",
+ disabled = false,
+ error = undefined,
+ focused: visuallyFocused,
+ fullWidth = false,
+ hiddenLabel = false,
+ margin = "none",
+ required = false,
+ size = "medium",
+ variant = "filled",
+ children,
+}: Partial<Props>): VNode {
+ const [filled, setFilled] = useState(false);
+ const [focusedState, setFocused] = useState(visuallyFocused);
+ const focused =
+ focusedState !== undefined && !disabled ? focusedState : false;
+
+ const value: FCCProps = {
+ color,
+ disabled,
+ error,
+ filled,
+ focused,
+ fullWidth,
+ hiddenLabel,
+ size,
+ onBlur: () => {
+ setFocused(false);
+ },
+ onEmpty: () => {
+ setFilled(false);
+ },
+ onFilled: () => {
+ setFilled(true);
+ },
+ onFocus: () => {
+ setFocused(true);
+ },
+ required,
+ variant,
+ };
+
+ return (
+ <div
+ class={[
+ root,
+ marginVariant[margin],
+ fullWidth ? fullWidthStyle : "",
+ ].join(" ")}
+ >
+ <FormControlContext.Provider value={value}>
+ {children}
+ </FormControlContext.Provider>
+ </div>
+ );
+}
+
+export interface FCCProps {
+ // adornedStart,
+ // setAdornedStart,
+ color: Colors;
+ disabled: boolean;
+ error: string | undefined | Error;
+ filled: boolean;
+ focused: boolean;
+ fullWidth: boolean;
+ hiddenLabel: boolean;
+ size: "medium" | "small";
+ onBlur: () => void;
+ onEmpty: () => void;
+ onFilled: () => void;
+ onFocus: () => void;
+ // registerEffect,
+ required: boolean;
+ variant: "filled" | "outlined" | "standard";
+}
+
+const defaultContextValue: FCCProps = {
+ color: "primary",
+ disabled: false,
+ error: undefined,
+ filled: false,
+ focused: false,
+ fullWidth: false,
+ hiddenLabel: false,
+ size: "medium",
+ onBlur: () => null,
+ onEmpty: () => null,
+ onFilled: () => null,
+ onFocus: () => null,
+ required: false,
+ variant: "filled",
+};
+
+function withoutUndefinedProperties(obj: any): any {
+ return Object.keys(obj).reduce((acc, key) => {
+ const _acc: any = acc;
+ if (obj[key] !== undefined && obj[key] !== false) _acc[key] = obj[key];
+ return _acc;
+ }, {});
+}
+
+export function useFormControl(props: Partial<FCCProps> = {}): FCCProps {
+ const ctx = useContext(FormControlContext);
+ const cleanedProps = withoutUndefinedProperties(props);
+
+ return useMemo(() => {
+ return !ctx
+ ? { ...defaultContextValue, ...cleanedProps }
+ : { ...ctx, ...cleanedProps };
+ }, [cleanedProps, ctx]);
+}
diff --git a/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx b/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx
new file mode 100644
index 000000000..3b80b0f23
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx
@@ -0,0 +1,70 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { css } from "@linaria/core";
+import { ComponentChildren, h, VNode } from "preact";
+// eslint-disable-next-line import/extensions
+import { theme } from "../style.js";
+import { useFormControl } from "./FormControl.js";
+
+const root = css`
+ color: ${theme.palette.text.secondary};
+ text-align: left;
+ margin-top: 3px;
+ margin-bottom: 0px;
+ margin-right: 0px;
+ margin-left: 0px;
+`;
+const disabledStyle = css`
+ color: ${theme.palette.text.disabled};
+`;
+const errorStyle = css`
+ color: ${theme.palette.error.main};
+`;
+const sizeSmallStyle = css`
+ margin-top: 4px;
+`;
+const containedStyle = css`
+ margin-right: 14px;
+ margin-left: 14px;
+`;
+
+interface Props {
+ disabled?: boolean;
+ error?: string | Error;
+ filled?: boolean;
+ focused?: boolean;
+ margin?: "dense";
+ required?: boolean;
+ children: ComponentChildren;
+}
+export function FormHelperText({ children, ...props }: Props): VNode {
+ const fcs = useFormControl(props);
+ const contained = fcs.variant === "filled" || fcs.variant === "outlined";
+ return (
+ <p
+ class={[
+ root,
+ theme.typography.caption,
+ fcs.disabled && disabledStyle,
+ fcs.error && errorStyle,
+ fcs.size === "small" && sizeSmallStyle,
+ contained && containedStyle,
+ ].join(" ")}
+ >
+ {children}
+ </p>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx b/packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx
new file mode 100644
index 000000000..68fbdc38e
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx
@@ -0,0 +1,85 @@
+/*
+ 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 { css } from "@linaria/core";
+import { ComponentChildren, h, VNode } from "preact";
+// eslint-disable-next-line import/extensions
+import { Colors, theme } from "../style.js";
+import { useFormControl } from "./FormControl.js";
+
+export interface Props {
+ class?: string;
+ disabled?: boolean;
+ error?: string;
+ filled?: boolean;
+ focused?: boolean;
+ required?: boolean;
+ color?: Colors;
+ children?: ComponentChildren;
+}
+
+const root = css`
+ color: ${theme.palette.text.secondary};
+ line-height: 1.4375em;
+ padding: 0px;
+ position: relative;
+ &[data-focused] {
+ color: var(--color-main);
+ }
+ &[data-disabled] {
+ color: ${theme.palette.text.disabled};
+ }
+ &[data-error] {
+ color: ${theme.palette.error.main};
+ }
+`;
+
+export function FormLabel({
+ disabled,
+ error,
+ filled,
+ focused,
+ required,
+ color,
+ class: _class,
+ children,
+ ...rest
+}: Props): VNode {
+ const fcs = useFormControl({
+ disabled,
+ error,
+ filled,
+ focused,
+ required,
+ color,
+ });
+ return (
+ <label
+ data-focused={!fcs.focused ? undefined : true}
+ data-error={!fcs.error ? undefined : true}
+ data-disabled={!fcs.disabled ? undefined : true}
+ class={[_class, root, theme.typography.body1].join(" ")}
+ {...rest}
+ style={{
+ "--color-main": theme.palette[fcs.color].main,
+ }}
+ >
+ {children}
+ {fcs.required && (
+ <span data-error={!fcs.error ? undefined : true}>&thinsp;{"*"}</span>
+ )}
+ </label>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
new file mode 100644
index 000000000..d811a3dbb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
@@ -0,0 +1,562 @@
+/*
+ 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 { css } from "@linaria/core";
+import { Fragment, h, JSX, VNode } from "preact";
+import {
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useRef,
+ useState,
+} from "preact/hooks";
+// eslint-disable-next-line import/extensions
+import { theme } from "../style.js";
+import { FormControlContext, useFormControl } from "./FormControl.js";
+
+const rootStyle = css`
+ color: ${theme.palette.text.primary};
+ line-height: 1.4375em;
+ box-sizing: border-box;
+ position: relative;
+ cursor: text;
+ display: inline-flex;
+ align-items: center;
+`;
+const rootDisabledStyle = css`
+ color: ${theme.palette.text.disabled};
+ cursor: default;
+`;
+const rootMultilineStyle = css`
+ padding: 4px 0 5px;
+`;
+const fullWidthStyle = css`
+ width: "100%";
+`;
+
+export function InputBaseRoot({
+ class: _class,
+ disabled,
+ error,
+ multiline,
+ focused,
+ fullWidth,
+ startAdornment,
+ endAdornment,
+ children,
+}: any): VNode {
+ const fcs = useFormControl({});
+ return (
+ <div
+ data-disabled={!disabled ? undefined : true}
+ data-focused={!focused ? undefined : true}
+ data-multiline={multiline}
+ data-hasStart={!!startAdornment}
+ data-hasEnd={!!endAdornment}
+ data-error={!error ? undefined : true}
+ class={[
+ _class,
+ rootStyle,
+ theme.typography.body1,
+ disabled && rootDisabledStyle,
+ multiline && rootMultilineStyle,
+ fullWidth && fullWidthStyle,
+ ].join(" ")}
+ style={{
+ "--color-main": theme.palette[fcs.color].main,
+ }}
+ >
+ {children}
+ </div>
+ );
+}
+
+const componentStyle = css`
+ font: inherit;
+ letter-spacing: inherit;
+ color: currentColor;
+ border: 0px;
+ box-sizing: content-box;
+ background: none;
+ height: 1.4375em;
+ margin: 0px;
+ -webkit-tap-highlight-color: transparent;
+ display: block;
+ min-width: 0px;
+ width: 100%;
+ animation-name: "auto-fill-cancel";
+ animation-duration: 10ms;
+
+ @keyframes auto-fill {
+ from {
+ display: block;
+ }
+ }
+ @keyframes auto-fill-cancel {
+ from {
+ display: block;
+ }
+ }
+ &::placeholder {
+ color: "currentColor";
+ opacity: ${theme.palette.mode === "light" ? 0.42 : 0.5};
+ transition: ${theme.transitions.create("opacity", {
+ duration: theme.transitions.duration.shorter,
+ })};
+ }
+ &:not(focus)::placeholder {
+ opacity: 0;
+ }
+ &:focus::placeholder {
+ opacity: ${theme.palette.mode === "light" ? 0.42 : 0.5};
+ }
+ &:focus {
+ outline: 0;
+ }
+ &:invalid {
+ box-shadow: none;
+ }
+ &::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ &:-webkit-autofill {
+ animation-duration: 5000s;
+ animation-name: auto-fill;
+ }
+ textarea {
+ height: "auto";
+ resize: "none";
+ padding: 0px;
+ padding-top: 0px;
+ }
+`;
+const componentDisabledStyle = css`
+ opacity: 1;
+ --webkit-text-fill-color: ${theme.palette.text.disabled};
+`;
+const componentSmallStyle = css`
+ padding-top: 1px;
+`;
+const componentMultilineStyle = css`
+ height: auto;
+ resize: none;
+ padding: 0px;
+ padding-top: 0px;
+`;
+const searchStyle = css`
+ -moz-appearance: textfield;
+ -webkit-appearance: textfield;
+`;
+
+export function InputBaseComponent({
+ disabled,
+ size,
+ multiline,
+ type,
+ class: _class,
+ startAdornment,
+ endAdornment,
+ ...props
+}: any): VNode {
+ return (
+ <Fragment>
+ {startAdornment}
+ <input
+ disabled={disabled}
+ type={type}
+ class={[
+ componentStyle,
+ _class,
+ disabled && componentDisabledStyle,
+ size === "small" && componentSmallStyle,
+ // multiline && componentMultilineStyle,
+ type === "search" && searchStyle,
+ ].join(" ")}
+ {...props}
+ />
+ {endAdornment}
+ </Fragment>
+ );
+}
+
+export function InputBase({
+ Root = InputBaseRoot,
+ Input,
+ onChange,
+ onInput,
+ name,
+ placeholder,
+ readOnly,
+ onKeyUp,
+ onKeyDown,
+ rows,
+ type = "text",
+ value,
+ maxRows,
+ minRows,
+ onClick,
+ ...props
+}: any): VNode {
+ const fcs = useFormControl(props);
+ // const [focused, setFocused] = useState(false);
+ useLayoutEffect(() => {
+ if (value && value !== "") {
+ fcs.onFilled();
+ } else {
+ fcs.onEmpty();
+ }
+ }, [value, fcs]);
+
+ const handleFocus = (event: JSX.TargetedFocusEvent<EventTarget>): void => {
+ // Fix a bug with IE11 where the focus/blur events are triggered
+ // while the component is disabled.
+ if (fcs.disabled) {
+ event.stopPropagation();
+ return;
+ }
+
+ // if (onFocus) {
+ // onFocus(event);
+ // }
+ // if (inputPropsProp.onFocus) {
+ // inputPropsProp.onFocus(event);
+ // }
+
+ fcs.onFocus();
+ };
+
+ const handleBlur = (): void => {
+ // if (onBlur) {
+ // onBlur(event);
+ // }
+ // if (inputPropsProp.onBlur) {
+ // inputPropsProp.onBlur(event);
+ // }
+
+ fcs.onBlur();
+ };
+
+ const handleChange = (
+ event: JSX.TargetedEvent<HTMLElement & { value?: string }>,
+ ): void => {
+ // if (inputPropsProp.onChange) {
+ // inputPropsProp.onChange(event, ...args);
+ // }
+
+ // Perform in the willUpdate
+ if (onChange) {
+ onChange(event.currentTarget.value);
+ }
+ };
+
+ const handleInput = (
+ event: JSX.TargetedEvent<HTMLElement & { value?: string }>,
+ ): void => {
+ // if (inputPropsProp.onChange) {
+ // inputPropsProp.onChange(event, ...args);
+ // }
+
+ // Perform in the willUpdate
+ if (onInput) {
+ event.currentTarget.value = onInput(event.currentTarget.value);
+ }
+ };
+
+ const handleClick = (
+ event: JSX.TargetedMouseEvent<HTMLElement & { value?: string }>,
+ ): void => {
+ // if (inputRef.current && event.currentTarget === event.target) {
+ // inputRef.current.focus();
+ // }
+
+ if (onClick) {
+ onClick(event.currentTarget.value);
+ }
+ };
+
+ const rowsProps = {
+ minRows: rows ? rows : minRows,
+ maxRows: rows ? rows : maxRows,
+ };
+ if (props.multiline) {
+ Input = TextareaAutoSize;
+ }
+
+ return (
+ <Root {...fcs} onClick={handleClick}>
+ <FormControlContext.Provider value={null}>
+ <Input
+ aria-invalid={fcs.error ? true : undefined}
+ // aria-describedby={}
+ disabled={fcs.disabled ? true : undefined}
+ name={name}
+ placeholder={!placeholder ? undefined : placeholder}
+ readOnly={readOnly}
+ required={fcs.required}
+ rows={rows}
+ value={value}
+ onKeyDown={onKeyDown}
+ onKeyUp={onKeyUp}
+ type={type}
+ onInput={handleInput}
+ onChange={handleChange}
+ onBlur={handleBlur}
+ onFocus={handleFocus}
+ {...rowsProps}
+ {...props}
+ />
+ </FormControlContext.Provider>
+ </Root>
+ );
+}
+const shadowStyle = css`
+ visibility: hidden;
+ position: absolute;
+ overflow: hidden;
+ height: 0px;
+ top: 0px;
+ left: 0px;
+ transform: translateZ(0);
+`;
+
+function ownerDocument(node: Node | null | undefined): Document {
+ return (node && node.ownerDocument) || document;
+}
+function ownerWindow(node: Node | null | undefined): Window {
+ const doc = ownerDocument(node);
+ return doc.defaultView || window;
+}
+function getStyleValue(
+ computedStyle: CSSStyleDeclaration,
+ property: any,
+): number {
+ return parseInt(computedStyle[property], 10) || 0;
+}
+
+function debounce(func: any, wait = 166): any {
+ let timeout: any;
+ function debounced(...args: any[]): void {
+ const later = () => {
+ func.apply({}, args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ }
+
+ debounced.clear = () => {
+ clearTimeout(timeout);
+ };
+
+ return debounced;
+}
+
+export function TextareaAutoSize({
+ // disabled,
+ // size,
+ onChange,
+ onInput,
+ value,
+ multiline,
+ focused,
+ disabled,
+ error,
+ minRows = 1,
+ maxRows,
+ style,
+ type,
+ class: _class,
+ ...props
+}: any): VNode {
+ // const { onChange, maxRows, minRows = 1, style, value, ...other } = props;
+
+ const { current: isControlled } = useRef(value != null);
+ const inputRef = useRef<HTMLTextAreaElement>(null);
+ // const handleRef = useForkRef(ref, inputRef);
+ const shadowRef = useRef<HTMLTextAreaElement>(null);
+ const renders = useRef(0);
+ const [state, setState] = useState<{ outerHeightStyle: any; overflow: any }>({
+ outerHeightStyle: undefined,
+ overflow: undefined,
+ });
+
+ const syncHeight = useCallback(() => {
+ const input = inputRef.current;
+ const inputShallow = shadowRef.current;
+ if (!input || !inputShallow) return;
+ const containerWindow = ownerWindow(input);
+ const computedStyle = containerWindow.getComputedStyle(input);
+
+ // If input's width is shrunk and it's not visible, don't sync height.
+ if (computedStyle.width === "0px") {
+ return;
+ }
+
+ inputShallow.style.width = computedStyle.width;
+ inputShallow.value = input.value || props.placeholder || "x";
+ if (inputShallow.value.slice(-1) === "\n") {
+ // Certain fonts which overflow the line height will cause the textarea
+ // to report a different scrollHeight depending on whether the last line
+ // is empty. Make it non-empty to avoid this issue.
+ inputShallow.value += " ";
+ }
+
+ const boxSizing: string = computedStyle["box-sizing" as any];
+ const padding =
+ getStyleValue(computedStyle, "padding-bottom") +
+ getStyleValue(computedStyle, "padding-top");
+ const border =
+ getStyleValue(computedStyle, "border-bottom-width") +
+ getStyleValue(computedStyle, "border-top-width");
+
+ // The height of the inner content
+ const innerHeight = inputShallow.scrollHeight;
+
+ // Measure height of a textarea with a single row
+ inputShallow.value = "x";
+ const singleRowHeight = inputShallow.scrollHeight;
+
+ // The height of the outer content
+ let outerHeight = innerHeight;
+
+ if (minRows) {
+ outerHeight = Math.max(Number(minRows) * singleRowHeight, outerHeight);
+ }
+ if (maxRows) {
+ outerHeight = Math.min(Number(maxRows) * singleRowHeight, outerHeight);
+ }
+ outerHeight = Math.max(outerHeight, singleRowHeight);
+
+ // Take the box sizing into account for applying this value as a style.
+ const outerHeightStyle =
+ outerHeight + (boxSizing === "border-box" ? padding + border : 0);
+ const overflow = Math.abs(outerHeight - innerHeight) <= 1;
+
+ setState((prevState) => {
+ // Need a large enough difference to update the height.
+ // This prevents infinite rendering loop.
+ if (
+ renders.current < 20 &&
+ ((outerHeightStyle > 0 &&
+ Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) ||
+ prevState.overflow !== overflow)
+ ) {
+ renders.current += 1;
+ return {
+ overflow,
+ outerHeightStyle,
+ };
+ }
+
+ return prevState;
+ });
+ }, [maxRows, minRows, props.placeholder]);
+
+ useLayoutEffect(() => {
+ const handleResize = debounce(() => {
+ renders.current = 0;
+ syncHeight();
+ });
+ const containerWindow = ownerWindow(inputRef.current);
+ containerWindow.addEventListener("resize", handleResize);
+ let resizeObserver: any;
+
+ if (typeof ResizeObserver !== "undefined") {
+ resizeObserver = new ResizeObserver(handleResize);
+ resizeObserver.observe(inputRef.current);
+ }
+
+ return () => {
+ handleResize.clear();
+ containerWindow.removeEventListener("resize", handleResize);
+ if (resizeObserver) {
+ resizeObserver.disconnect();
+ }
+ };
+ }, [syncHeight]);
+
+ useLayoutEffect(() => {
+ syncHeight();
+ });
+
+ useLayoutEffect(() => {
+ renders.current = 0;
+ }, [value]);
+
+ const handleChange = (event: any): void => {
+ renders.current = 0;
+
+ if (!isControlled) {
+ syncHeight();
+ }
+
+ if (onChange) {
+ onChange(event.target.value);
+ }
+ };
+ const handleInput = (event: any): void => {
+ renders.current = 0;
+
+ if (!isControlled) {
+ syncHeight();
+ }
+
+ if (onInput) {
+ event.currentTarget.value = onInput(event.currentTarget.value);
+ }
+ };
+
+ return (
+ <Fragment>
+ <textarea
+ class={[
+ componentStyle,
+ componentMultilineStyle,
+ _class,
+ disabled && componentDisabledStyle,
+ // size === "small" && componentSmallStyle,
+ multiline && componentMultilineStyle,
+ type === "search" && searchStyle,
+ ].join(" ")}
+ value={value}
+ onChange={handleChange}
+ onInput={handleInput}
+ ref={inputRef}
+ // Apply the rows prop to get a "correct" first SSR paint
+ rows={minRows}
+ style={{
+ height: state.outerHeightStyle,
+ // Need a large enough difference to allow scrolling.
+ // This prevents infinite rendering loop.
+ overflow: state.overflow ? "hidden" : null,
+ ...style,
+ }}
+ {...props}
+ />
+
+ <textarea
+ aria-hidden
+ class={[
+ componentStyle,
+ componentMultilineStyle,
+ shadowStyle,
+ type === "search" && searchStyle,
+ ].join(" ")}
+ readOnly
+ ref={shadowRef}
+ tabIndex={-1}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx b/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx
new file mode 100644
index 000000000..0707046f3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx
@@ -0,0 +1,199 @@
+/*
+ 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 { css } from "@linaria/core";
+import { h, VNode } from "preact";
+// eslint-disable-next-line import/extensions
+import { Colors, theme } from "../style.js";
+import { useFormControl } from "./FormControl.js";
+import { InputBase, InputBaseComponent, InputBaseRoot } from "./InputBase.js";
+
+export interface Props {
+ autoComplete?: string;
+ autoFocus?: boolean;
+ color?: Colors;
+ defaultValue?: string;
+ disabled?: boolean;
+ disableUnderline?: boolean;
+ error?: string | Error;
+ fullWidth?: boolean;
+ id?: string;
+ margin?: "dense" | "normal" | "none";
+ maxRows?: number;
+ minRows?: number;
+ multiline?: boolean;
+ name?: string;
+ onChange?: (s: string) => void;
+ placeholder?: string;
+ readOnly?: boolean;
+ required?: boolean;
+ rows?: number;
+ startAdornment?: VNode;
+ endAdornment?: VNode;
+ type?: string;
+ value?: string;
+}
+export function InputFilled({
+ type = "text",
+ multiline,
+ ...props
+}: Props): VNode {
+ const fcs = useFormControl(props);
+ return (
+ <InputBase
+ Root={Root}
+ Input={Input}
+ fullWidth={fcs.fullWidth}
+ multiline={multiline}
+ type={type}
+ {...props}
+ />
+ );
+}
+
+const light = theme.palette.mode === "light";
+const bottomLineColor = light
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)";
+const backgroundColor = light
+ ? "rgba(0, 0, 0, 0.06)"
+ : "rgba(255, 255, 255, 0.09)";
+const backgroundColorHover = light
+ ? "rgba(0, 0, 0, 0.09)"
+ : "rgba(255, 255, 255, 0.13)";
+const backgroundColorDisabled = light
+ ? "rgba(0, 0, 0, 0.12)"
+ : "rgba(255, 255, 255, 0.12)";
+
+const formControlStyle = css`
+ label + & {
+ margin-top: 16px;
+ }
+`;
+
+const filledRootStyle = css`
+ position: relative;
+ background-color: ${backgroundColor};
+ border-top-left-radius: ${theme.shape.borderRadius}px;
+ border-top-right-radius: ${theme.shape.borderRadius}px;
+ transition: ${theme.transitions.create("background-color", {
+ duration: theme.transitions.duration.shorter,
+ easing: theme.transitions.easing.easeOut,
+})};
+ // when is not disabled underline
+ &:hover {
+ background-color: ${backgroundColorHover};
+ @media (hover: none) {
+ background-color: ${backgroundColor};
+ }
+ }
+ &[data-focused] {
+ background-color: ${backgroundColor};
+ }
+ &[data-disabled] {
+ background-color: ${backgroundColorDisabled};
+ }
+ &[data-multiline] {
+ padding: 25px 12px 8px;
+ }
+ /* &[data-hasStart] {
+ padding-left: 25px;
+ } */
+`;
+
+const underlineStyle = css`
+ // when is not disabled underline
+ &:after {
+ border-bottom: 2px solid var(--color-main);
+ left: 0px;
+ bottom: 0px;
+ content: "";
+ position: absolute;
+ right: 0px;
+ transform: scaleX(0);
+ transition: ${theme.transitions.create("transform", {
+ duration: theme.transitions.duration.shorter,
+ easing: theme.transitions.easing.easeOut,
+})};
+ pointer-events: none;
+ }
+ &[data-focused]:after {
+ transform: scaleX(1);
+ }
+ &[data-error]:after {
+ border-bottom-color: ${theme.palette.error.main};
+ transform: scaleY(1);
+ }
+ &:before {
+ border-bottom: 1px solid
+ ${theme.palette.mode === "light"
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
+ left: 0px;
+ bottom: 0px;
+ right: 0px;
+ content: "\\00a0";
+ position: absolute;
+ transition: ${theme.transitions.create("border-bottom-color", {
+ duration: theme.transitions.duration.shorter,
+ })};
+ pointer-events: none;
+ }
+ &:hover:not([data-disabled]:before) {
+ border-bottom: 2px solid var(--color-main);
+ @media (hover: none) {
+ border-bottom: 1px solid
+ ${theme.palette.mode === "light"
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
+ }
+ }
+ &[data-disabled]:before {
+ border-bottom-style: solid;
+ }
+`;
+
+function Root({
+ fullWidth,
+ disabled,
+ focused,
+ error,
+ children,
+ multiline,
+}: any): VNode {
+ return (
+ <InputBaseRoot
+ disabled={disabled}
+ focused={focused ? true : undefined}
+ fullWidth={fullWidth}
+ multiline={multiline}
+ error={error}
+ class={[filledRootStyle, underlineStyle].join(" ")}
+ >
+ {children}
+ </InputBaseRoot>
+ );
+}
+
+const filledBaseStyle = css`
+ padding-top: 25px;
+ padding-right: 12px;
+ padding-bottom: 8px;
+ padding-left: 12px;
+`;
+
+function Input(props: any): VNode {
+ return <InputBaseComponent class={[filledBaseStyle].join(" ")} {...props} />;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx b/packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx
new file mode 100644
index 000000000..2d4743e59
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx
@@ -0,0 +1,114 @@
+/*
+ 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 { css } from "@linaria/core";
+import { ComponentChildren, h, VNode } from "preact";
+// eslint-disable-next-line import/extensions
+import { Colors, theme } from "../style.js";
+import { useFormControl } from "./FormControl.js";
+import { FormLabel } from "./FormLabel.js";
+
+const root = css`
+ display: block;
+ transform-origin: top left;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+
+ &[data-form-control] {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ transform: translate(0, 20px) scale(1);
+ }
+ &[data-size="small"] {
+ transform: translate(0, 17px) scale(1);
+ }
+ &[data-shrink] {
+ transform: translate(0, -1.5px) scale(0.75);
+ transform-origin: top left;
+ max-width: 133%;
+ }
+ &:not([data-disable-animation]) {
+ transition: ${theme.transitions.create(
+ ["color", "transform", "max-width"],
+ {
+ duration: theme.transitions.duration.shorter,
+ easing: theme.transitions.easing.easeOut,
+ },
+ )};
+ }
+ &[data-variant="filled"] {
+ z-index: 1;
+ pointer-events: none;
+ transform: translate(12px, 16px) scale(1);
+ max-width: calc(100% - 24px);
+ &[data-size="small"] {
+ transform: translate(12px, 13px) scale(1);
+ }
+ &[data-shrink] {
+ user-select: none;
+ pointer-events: auto;
+ transform: translate(12px, 7px) scale(0.75);
+ max-width: calc(133% - 24px);
+ &[data-size="small"] {
+ transform: translate(12px, 4px) scale(0.75);
+ }
+ }
+ }
+ &[data-variant="outlined"] {
+ z-index: 1;
+ pointer-events: none;
+ transform: translate(14px, 16px) scale(1);
+ max-width: calc(100% - 24px);
+ &[data-size="small"] {
+ transform: translate(14px, 9px) scale(1);
+ }
+ &[data-shrink] {
+ user-select: none;
+ pointer-events: auto;
+ transform: translate(14px, -9px) scale(0.75);
+ max-width: calc(133% - 24px);
+ }
+ }
+`;
+
+interface InputLabelProps {
+ color: Colors;
+ disableAnimation: boolean;
+ disabled: boolean;
+ error?: string;
+ focused: boolean;
+ margin: boolean;
+ required: boolean;
+ shrink: boolean;
+ variant: "filled" | "outlined" | "standard";
+ children: ComponentChildren;
+}
+export function InputLabel(props: Partial<InputLabelProps>): VNode {
+ const fcs = useFormControl(props);
+ return (
+ <FormLabel
+ data-form-control={!!fcs}
+ data-size={fcs.size}
+ data-shrink={props.shrink || fcs.filled || fcs.focused ? true : undefined}
+ data-disable-animation={props.disableAnimation ? true : undefined}
+ data-variant={fcs.variant}
+ class={root}
+ {...props}
+ />
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx b/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx
new file mode 100644
index 000000000..7352c5ec1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx
@@ -0,0 +1,142 @@
+/*
+ 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 { css } from "@linaria/core";
+import { h, VNode } from "preact";
+import { Colors, theme } from "../style.js";
+import { useFormControl } from "./FormControl.js";
+import { InputBase, InputBaseComponent, InputBaseRoot } from "./InputBase.js";
+
+export interface Props {
+ autoComplete?: string;
+ autoFocus?: boolean;
+ color?: Colors;
+ defaultValue?: string;
+ disabled?: boolean;
+ disableUnderline?: boolean;
+ endAdornment?: VNode;
+ error?: string | Error;
+ fullWidth?: boolean;
+ id?: string;
+ margin?: "dense" | "normal" | "none";
+ maxRows?: number;
+ minRows?: number;
+ multiline?: boolean;
+ name?: string;
+ onChange?: (s: string) => void;
+ placeholder?: string;
+ readOnly?: boolean;
+ required?: boolean;
+ rows?: number;
+ startAdornment?: VNode;
+ type?: string;
+ value?: string;
+}
+export function InputStandard({
+ type = "text",
+ multiline,
+ ...props
+}: Props): VNode {
+ const fcs = useFormControl(props);
+ return (
+ <InputBase
+ Root={Root}
+ Input={Input}
+ fullWidth={fcs.fullWidth}
+ multiline={multiline}
+ type={type}
+ {...props}
+ />
+ );
+}
+
+const rootStyle = css`
+ position: relative;
+ padding: 4px 0 5px;
+`;
+const formControlStyle = css`
+ label + & {
+ margin-top: 16px;
+ }
+`;
+const underlineStyle = css`
+ // when is not disabled underline
+ &:after {
+ border-bottom: 2px solid var(--color-main);
+ left: 0px;
+ bottom: 0px;
+ content: "";
+ position: absolute;
+ right: 0px;
+ transform: scaleX(0);
+ transition: ${theme.transitions.create("transform", {
+ duration: theme.transitions.duration.shorter,
+ easing: theme.transitions.easing.easeOut,
+})};
+ pointer-events: none;
+ }
+ &[data-focused]:after {
+ transform: scaleX(1);
+ }
+ &[data-error]:after {
+ border-bottom-color: ${theme.palette.error.main};
+ transform: scaleY(1);
+ }
+ &:before {
+ border-bottom: 1px solid
+ ${theme.palette.mode === "light"
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
+ left: 0px;
+ bottom: 0px;
+ right: 0px;
+ content: "\\00a0";
+ position: absolute;
+ transition: ${theme.transitions.create("border-bottom-color", {
+ duration: theme.transitions.duration.shorter,
+ })};
+ pointer-events: none;
+ }
+ &:hover:not([data-disabled]:before) {
+ border-bottom: 2px solid var(--color-main);
+ @media (hover: none) {
+ border-bottom: 1px solid
+ ${theme.palette.mode === "light"
+ ? "rgba(0, 0, 0, 0.42)"
+ : "rgba(255, 255, 255, 0.7)"};
+ }
+ }
+ &[data-disabled]:before {
+ border-bottom-style: solid;
+ }
+`;
+
+function Root({ fullWidth, disabled, focused, error, children }: any): VNode {
+ return (
+ <InputBaseRoot
+ disabled={disabled}
+ focused={focused ? true : undefined}
+ fullWidth={fullWidth}
+ error={error}
+ class={[rootStyle, formControlStyle, underlineStyle].join(" ")}
+ >
+ {children}
+ </InputBaseRoot>
+ );
+}
+
+function Input(props: any): VNode {
+ return <InputBaseComponent {...props} />;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/input/SelectFilled.tsx b/packages/taler-wallet-webextension/src/mui/input/SelectFilled.tsx
new file mode 100644
index 000000000..0ae70d06a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/SelectFilled.tsx
@@ -0,0 +1,20 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, VNode } from "preact";
+
+export function SelectFilled(): VNode {
+ return <div />;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/input/SelectOutlined.tsx b/packages/taler-wallet-webextension/src/mui/input/SelectOutlined.tsx
new file mode 100644
index 000000000..72579aed2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/SelectOutlined.tsx
@@ -0,0 +1,20 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, VNode } from "preact";
+
+export function SelectOutlined(): VNode {
+ return <div />;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/input/SelectStandard.tsx b/packages/taler-wallet-webextension/src/mui/input/SelectStandard.tsx
new file mode 100644
index 000000000..b0474a80b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/SelectStandard.tsx
@@ -0,0 +1,200 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { css } from "@linaria/core";
+import { h, VNode, Fragment } from "preact";
+import { useRef } from "preact/hooks";
+import { Paper } from "../Paper.js";
+
+function hasValue(value: any): boolean {
+ return value != null && !(Array.isArray(value) && value.length === 0);
+}
+
+const SelectSelect = css`
+ height: "auto";
+ min-height: "1.4374em";
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+`;
+
+const SelectIcon = css``;
+
+const SelectNativeInput = css`
+ bottom: 0px;
+ left: 0px;
+ position: "absolute";
+ opacity: 0px;
+ pointer-events: "none";
+ width: 100%;
+ box-sizing: border-box;
+`;
+
+// export function SelectStandard({ value }: any): VNode {
+// return (
+// <Fragment>
+// <div class={SelectSelect} role="button">
+// {!value ? (
+// // notranslate needed while Google Translate will not fix zero-width space issue
+// <span className="notranslate">&#8203;</span>
+// ) : (
+// value
+// )}
+// <input
+// class={SelectNativeInput}
+// aria-hidden
+// tabIndex={-1}
+// value={Array.isArray(value) ? value.join(",") : value}
+// />
+// </div>
+// </Fragment>
+// );
+// }
+function isFilled(obj: any, SSR = false): boolean {
+ return (
+ obj &&
+ ((hasValue(obj.value) && obj.value !== "") ||
+ (SSR && hasValue(obj.defaultValue) && obj.defaultValue !== ""))
+ );
+}
+function isEmpty(display: any): boolean {
+ return display == null || (typeof display === "string" && !display.trim());
+}
+
+export function SelectStandard({
+ value,
+ multiple,
+ displayEmpty,
+ onBlur,
+ onChange,
+ onClose,
+ onFocus,
+ onOpen,
+ renderValue,
+ menuMinWidthState,
+}: any): VNode {
+ const inputRef = useRef(null);
+ const displayRef = useRef(null);
+
+ let display;
+ let computeDisplay = false;
+ let foundMatch = false;
+ let displaySingle;
+ const displayMultiple: any[] = [];
+ if (isFilled({ value }) || displayEmpty) {
+ if (renderValue) {
+ display = renderValue(value);
+ } else {
+ computeDisplay = true;
+ }
+ }
+ if (computeDisplay) {
+ if (multiple) {
+ if (displayMultiple.length === 0) {
+ display = null;
+ } else {
+ display = displayMultiple.reduce((output, child, index) => {
+ output.push(child);
+ if (index < displayMultiple.length - 1) {
+ output.push(", ");
+ }
+ return output;
+ }, []);
+ }
+ } else {
+ display = displaySingle;
+ }
+ }
+
+ // Avoid performing a layout computation in the render method.
+ let menuMinWidth = menuMinWidthState;
+
+ // if (!autoWidth && isOpenControlled && displayNode) {
+ // menuMinWidth = displayNode.clientWidth;
+ // }
+
+ // let tabIndex;
+ // if (typeof tabIndexProp !== "undefined") {
+ // tabIndex = tabIndexProp;
+ // } else {
+ // tabIndex = disabled ? null : 0;
+ // }
+ const update = (open: any, event: any) => {
+ if (open) {
+ if (onOpen) {
+ onOpen(event);
+ }
+ } else if (onClose) {
+ onClose(event);
+ }
+
+ // if (!isOpenControlled) {
+ // setMenuMinWidthState(autoWidth ? null : displayNode.clientWidth);
+ // setOpenState(open);
+ // }
+ };
+
+ const handleMouseDown = (event: any) => {
+ // Ignore everything but left-click
+ if (event.button !== 0) {
+ return;
+ }
+ // Hijack the default focus behavior.
+ event.preventDefault();
+ // displayRef.current.focus();
+
+ update(true, event);
+ };
+ return (
+ <Fragment>
+ <div
+ class={css`
+ height: auto;
+ min-height: 14375em;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ `}
+ >
+ {isEmpty(display) ? (
+ // notranslate needed while Google Translate will not fix zero-width space issue
+ <span class="notranslate">&#8203;</span>
+ ) : (
+ display
+ )}
+ </div>
+ <input
+ class={css`
+ bottom: 0px;
+ left: 0px;
+ position: "absolute";
+ opacity: 0;
+ pointer-events: none;
+ width: 100%;
+ box-sizing: border-box;
+ `}
+ />
+ <svg />
+ </Fragment>
+ );
+}
+
+// function Popover(): VNode {
+// return;
+// }
+
+// function Menu(): VNode {
+// return <Paper></Paper>;
+// }
diff --git a/packages/taler-wallet-webextension/src/mui/style.tsx b/packages/taler-wallet-webextension/src/mui/style.tsx
new file mode 100644
index 000000000..99adf2a76
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/style.tsx
@@ -0,0 +1,874 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { css } from "@linaria/core";
+import { darken, lighten } from "polished";
+import {
+ blue,
+ common,
+ green,
+ grey,
+ lightBlue,
+ orange,
+ purple,
+ red,
+ // eslint-disable-next-line import/extensions
+} from "./colors/constants.js";
+// eslint-disable-next-line import/extensions
+import { getContrastRatio } from "./colors/manipulation.js";
+
+export type Colors =
+ | "primary"
+ | "secondary"
+ | "success"
+ | "error"
+ | "info"
+ | "warning";
+
+export function round(value: number): number {
+ return Math.round(value * 1e5) / 1e5;
+}
+const fontSize = 14;
+const htmlFontSize = 16;
+const coef = fontSize / 14;
+export function pxToRem(size: number): string {
+ return `${(size / htmlFontSize) * coef}rem`;
+}
+
+export interface Spacing {
+ (): string;
+ (value?: number): string;
+ (topBottom: number, rightLeft: number): string;
+ (top: number, rightLeft: number, bottom: number): string;
+ (top: number, right: number, bottom: number, left: number): string;
+}
+
+const zIndex = {
+ mobileStepper: 1000,
+ speedDial: 1050,
+ appBar: 1100,
+ drawer: 1200,
+ modal: 1300,
+ snackbar: 1400,
+ tooltip: 1500,
+};
+
+export const theme = createTheme();
+
+export const ripple = css`
+ background-position: center;
+
+ transition: background 0.2s;
+
+ &:hover {
+ background: var(--color-main)
+ radial-gradient(circle, transparent 1%, var(--color-dark) 1%)
+ center/15000%;
+ }
+ &:active {
+ background-color: var(--color-main);
+ background-size: 100%;
+ transition: background 0s;
+ }
+`;
+
+export const rippleEnabled = css`
+ background-position: center;
+
+ transition: background 0.2s;
+
+ &:hover:enabled {
+ background: var(--color-main)
+ radial-gradient(circle, transparent 1%, var(--color-dark) 1%)
+ center/15000%;
+ }
+ &:active:enabled {
+ background-color: var(--color-main);
+ background-size: 100%;
+ transition: background 0s;
+ }
+`;
+
+export const rippleEnabledOutlined = css`
+ background-position: center;
+
+ transition: background 0.2s;
+
+ &:hover:enabled {
+ background: var(--color-contrastText)
+ radial-gradient(circle, transparent 1%, var(--color-light) 1%)
+ center/15000%;
+ }
+
+ &:active:enabled {
+ background-color: var(--color-contrastText);
+ background-size: 100%;
+ transition: background 0s;
+ }
+`;
+
+function createTheme() {
+ const light = {
+ // The colors used to style the text.
+ text: {
+ // The most important text.
+ primary: "rgba(0, 0, 0, 0.87)",
+ // Secondary text.
+ secondary: "rgba(0, 0, 0, 0.6)",
+ // Disabled text have even lower visual prominence.
+ disabled: "rgba(0, 0, 0, 0.38)",
+ },
+ // The color used to divide different elements.
+ divider: "rgba(0, 0, 0, 0.12)",
+ // The background colors used to style the surfaces.
+ // Consistency between these values is important.
+ background: {
+ paper: common.white,
+ default: common.white,
+ },
+ // The colors used to style the action elements.
+ action: {
+ // The color of an active action like an icon button.
+ active: "rgba(0, 0, 0, 0.54)",
+ // The color of an hovered action.
+ hover: "rgba(0, 0, 0, 0.04)",
+ hoverOpacity: 0.04,
+ // The color of a selected action.
+ selected: "rgba(0, 0, 0, 0.08)",
+ selectedOpacity: 0.08,
+ // The color of a disabled action.
+ disabled: "rgba(0, 0, 0, 0.26)",
+ // The background color of a disabled action.
+ disabledBackground: "rgba(0, 0, 0, 0.12)",
+ disabledOpacity: 0.38,
+ focus: "rgba(0, 0, 0, 0.12)",
+ focusOpacity: 0.12,
+ activatedOpacity: 0.12,
+ },
+ };
+
+ const dark = {
+ text: {
+ primary: common.white,
+ secondary: "rgba(255, 255, 255, 0.7)",
+ disabled: "rgba(255, 255, 255, 0.5)",
+ icon: "rgba(255, 255, 255, 0.5)",
+ },
+ divider: "rgba(255, 255, 255, 0.12)",
+ background: {
+ paper: "#121212",
+ default: "#121212",
+ },
+ action: {
+ active: common.white,
+ hover: "rgba(255, 255, 255, 0.08)",
+ hoverOpacity: 0.08,
+ selected: "rgba(255, 255, 255, 0.16)",
+ selectedOpacity: 0.16,
+ disabled: "rgba(255, 255, 255, 0.3)",
+ disabledBackground: "rgba(255, 255, 255, 0.12)",
+ disabledOpacity: 0.38,
+ focus: "rgba(255, 255, 255, 0.12)",
+ focusOpacity: 0.12,
+ activatedOpacity: 0.24,
+ },
+ };
+
+ const defaultFontFamily = '"Roboto", "Helvetica", "Arial", sans-serif';
+
+ const shadowKeyUmbraOpacity = 0.2;
+ const shadowKeyPenumbraOpacity = 0.14;
+ const shadowAmbientShadowOpacity = 0.12;
+
+ const typography = createTypography({});
+ const palette = createPalette({});
+ const shadows = createAllShadows();
+ const transitions = createTransitions({});
+ const breakpoints = createBreakpoints({});
+ const spacing = createSpacing();
+ const shape = {
+ roundBorder: css`
+ border-radius: 4px;
+ `,
+ squareBorder: css`
+ border-radius: 0px;
+ `,
+ circularBorder: css`
+ border-radius: 50%;
+ `,
+ borderRadius: 4,
+ };
+
+ /////////////////////
+ ///////////////////// SPACING
+ /////////////////////
+
+ function createUnaryUnit(theme: { spacing: number }, defaultValue: number) {
+ const themeSpacing = theme.spacing || defaultValue;
+
+ if (typeof themeSpacing === "number") {
+ return (abs: number | string) => {
+ if (typeof abs === "string") {
+ return abs;
+ }
+
+ return themeSpacing * abs;
+ };
+ }
+
+ if (Array.isArray(themeSpacing)) {
+ return (abs: number | string) => {
+ if (typeof abs === "string") {
+ return abs;
+ }
+
+ return themeSpacing[abs];
+ };
+ }
+
+ if (typeof themeSpacing === "function") {
+ return themeSpacing;
+ }
+
+ return (a: string | number) => "";
+ }
+
+ function createUnarySpacing(theme: { spacing: number }) {
+ return createUnaryUnit(theme, 8);
+ }
+
+ function createSpacing(spacingInput = 8): Spacing {
+ // Material Design layouts are visually balanced. Most measurements align to an 8dp grid, which aligns both spacing and the overall layout.
+ // Smaller components, such as icons, can align to a 4dp grid.
+ // https://material.io/design/layout/understanding-layout.html#usage
+ const transform = createUnarySpacing({
+ spacing: spacingInput,
+ });
+
+ const spacing = (
+ ...argsInput: ReadonlyArray<number | string | undefined>
+ ): string => {
+ const args = argsInput.length === 0 ? [1] : argsInput;
+
+ return args
+ .map((argument) => {
+ if (argument === undefined) return "";
+ const output = transform(argument);
+ return typeof output === "number" ? `${output}px` : output;
+ })
+ .join(" ");
+ };
+
+ return spacing;
+ }
+ /////////////////////
+ ///////////////////// BREAKPOINTS
+ /////////////////////
+ function createBreakpoints(breakpoints: any) {
+ const {
+ // The breakpoint **start** at this value.
+ // For instance with the first breakpoint xs: [xs, sm).
+ values = {
+ xs: 0,
+ sm: 600,
+ md: 900,
+ lg: 1200,
+ xl: 1536, // large screen
+ },
+ unit = "px",
+ step = 5,
+ // ...other
+ } = breakpoints;
+
+ const keys = Object.keys(values);
+
+ function up(key: any) {
+ const value = typeof values[key] === "number" ? values[key] : key;
+ return `@media (min-width:${value}${unit})`;
+ }
+
+ function down(key: any) {
+ const value = typeof values[key] === "number" ? values[key] : key;
+ return `@media (max-width:${value - step / 100}${unit})`;
+ }
+
+ function between(start: any, end: any) {
+ const endIndex = keys.indexOf(end);
+
+ return (
+ `@media (min-width:${
+ typeof values[start] === "number" ? values[start] : start
+ }${unit}) and ` +
+ `(max-width:${
+ (endIndex !== -1 && typeof values[keys[endIndex]] === "number"
+ ? values[keys[endIndex]]
+ : end) -
+ step / 100
+ }${unit})`
+ );
+ }
+
+ function only(key: any) {
+ if (keys.indexOf(key) + 1 < keys.length) {
+ return between(key, keys[keys.indexOf(key) + 1]);
+ }
+
+ return up(key);
+ }
+
+ function not(key: any) {
+ // handle first and last key separately, for better readability
+ const keyIndex = keys.indexOf(key);
+ if (keyIndex === 0) {
+ return up(keys[1]);
+ }
+ if (keyIndex === keys.length - 1) {
+ return down(keys[keyIndex]);
+ }
+
+ return between(key, keys[keys.indexOf(key) + 1]).replace(
+ "@media",
+ "@media not all and",
+ );
+ }
+
+ return {
+ keys,
+ values,
+ up,
+ down,
+ between,
+ only,
+ not,
+ unit,
+ // ...other,
+ };
+ }
+
+ /////////////////////
+ ///////////////////// SHADOWS
+ /////////////////////
+ function createShadow(...px: number[]): string {
+ return [
+ `${px[0]}px ${px[1]}px ${px[2]}px ${px[3]}px rgba(0,0,0,${shadowKeyUmbraOpacity})`,
+ `${px[4]}px ${px[5]}px ${px[6]}px ${px[7]}px rgba(0,0,0,${shadowKeyPenumbraOpacity})`,
+ `${px[8]}px ${px[9]}px ${px[10]}px ${px[11]}px rgba(0,0,0,${shadowAmbientShadowOpacity})`,
+ ].join(",");
+ }
+
+ function createAllShadows() {
+ // Values from https://github.com/material-components/material-components-web/blob/be8747f94574669cb5e7add1a7c54fa41a89cec7/packages/mdc-elevation/_variables.scss
+ return [
+ "none",
+ createShadow(0, 2, 1, -1, 0, 1, 1, 0, 0, 1, 3, 0),
+ createShadow(0, 3, 1, -2, 0, 2, 2, 0, 0, 1, 5, 0),
+ createShadow(0, 3, 3, -2, 0, 3, 4, 0, 0, 1, 8, 0),
+ createShadow(0, 2, 4, -1, 0, 4, 5, 0, 0, 1, 10, 0),
+ createShadow(0, 3, 5, -1, 0, 5, 8, 0, 0, 1, 14, 0),
+ createShadow(0, 3, 5, -1, 0, 6, 10, 0, 0, 1, 18, 0),
+ createShadow(0, 4, 5, -2, 0, 7, 10, 1, 0, 2, 16, 1),
+ createShadow(0, 5, 5, -3, 0, 8, 10, 1, 0, 3, 14, 2),
+ createShadow(0, 5, 6, -3, 0, 9, 12, 1, 0, 3, 16, 2),
+ createShadow(0, 6, 6, -3, 0, 10, 14, 1, 0, 4, 18, 3),
+ createShadow(0, 6, 7, -4, 0, 11, 15, 1, 0, 4, 20, 3),
+ createShadow(0, 7, 8, -4, 0, 12, 17, 2, 0, 5, 22, 4),
+ createShadow(0, 7, 8, -4, 0, 13, 19, 2, 0, 5, 24, 4),
+ createShadow(0, 7, 9, -4, 0, 14, 21, 2, 0, 5, 26, 4),
+ createShadow(0, 8, 9, -5, 0, 15, 22, 2, 0, 6, 28, 5),
+ createShadow(0, 8, 10, -5, 0, 16, 24, 2, 0, 6, 30, 5),
+ createShadow(0, 8, 11, -5, 0, 17, 26, 2, 0, 6, 32, 5),
+ createShadow(0, 9, 11, -5, 0, 18, 28, 2, 0, 7, 34, 6),
+ createShadow(0, 9, 12, -6, 0, 19, 29, 2, 0, 7, 36, 6),
+ createShadow(0, 10, 13, -6, 0, 20, 31, 3, 0, 8, 38, 7),
+ createShadow(0, 10, 13, -6, 0, 21, 33, 3, 0, 8, 40, 7),
+ createShadow(0, 10, 14, -6, 0, 22, 35, 3, 0, 8, 42, 7),
+ createShadow(0, 11, 14, -7, 0, 23, 36, 3, 0, 9, 44, 8),
+ createShadow(0, 11, 15, -7, 0, 24, 38, 3, 0, 9, 46, 8),
+ ];
+ }
+
+ /////////////////////
+ ///////////////////// TYPOGRAPHY
+ /////////////////////
+ /**
+ * @see @link{https://material.io/design/typography/the-type-system.html}
+ * @see @link{https://material.io/design/typography/understanding-typography.html}
+ */
+ function createTypography(typography: any) {
+ // const {
+ const fontFamily = defaultFontFamily,
+ // The default font size of the Material Specification.
+ fontSize = 14, // px
+ fontWeightLight = 300,
+ fontWeightRegular = 400,
+ fontWeightMedium = 500,
+ fontWeightBold = 700,
+ // Tell MUI what's the font-size on the html element.
+ // 16px is the default font-size used by browsers.
+ htmlFontSize = 16;
+ // Apply the CSS properties to all the variants.
+ // allVariants,
+ // pxToRem: pxToRem2,
+ // ...other
+ // } = typography;
+ const variants = {
+ // (fontWeight, size, lineHeight, letterSpacing, casing) =>
+ // h1: buildVariant(fontWeightLight, 96, 1.167, -1.5),
+ // h2: buildVariant(fontWeightLight, 60, 1.2, -0.5),
+ // h3: buildVariant(fontWeightRegular, 48, 1.167, 0),
+ // h4: buildVariant(fontWeightRegular, 34, 1.235, 0.25),
+ // h5: buildVariant(fontWeightRegular, 24, 1.334, 0),
+ // h6: buildVariant(fontWeightMedium, 20, 1.6, 0.15),
+ // subtitle1: buildVariant(fontWeightRegular, 16, 1.75, 0.15),
+ // subtitle2: buildVariant(fontWeightMedium, 14, 1.57, 0.1),
+ body1: css`
+ font-family: "Roboto", "Helvetica", "Arial", sans-serif;
+ font-weight: ${fontWeightRegular};
+ font-size: ${pxToRem(16)};
+ line-height: 1.5;
+ letter-spacing: ${round(0.15 / 16)}em;
+ `,
+ // body1: buildVariant(fontWeightRegular, 16, 1.5, 0.15),
+ body2: css`
+ font-family: "Roboto", "Helvetica", "Arial", sans-serif;
+ font-weight: ${fontWeightRegular};
+ font-size: ${pxToRem(14)};
+ line-height: 1.43;
+ letter-spacing: ${round(0.15 / 14)}em;
+ `,
+ // body2: buildVariant(fontWeightRegular, 14, 1.43, 0.15),
+ button: css`
+ font-family: "Roboto", "Helvetica", "Arial", sans-serif;
+ font-weight: ${fontWeightMedium};
+ font-size: ${pxToRem(14)};
+ line-height: 1.75;
+ letter-spacing: ${round(0.4 / 14)}em;
+ text-transform: uppercase;
+ `,
+ /* just of caseAllCaps */
+ // button: buildVariant(fontWeightMedium, 14, 1.75, 0.4, caseAllCaps),
+
+ caption: css`
+ font-family: "Roboto", "Helvetica", "Arial", sans-serif;
+ font-weight: ${fontWeightMedium};
+ font-size: ${pxToRem(12)};
+ line-height: 1.66;
+ letter-spacing: ${round(0.4 / 12)}em;
+ `,
+ // caption: buildVariant(fontWeightRegular, 12, 1.66, 0.4),
+ // overline: buildVariant(fontWeightRegular, 12, 2.66, 1, caseAllCaps),
+ };
+
+ return deepmerge(
+ {
+ htmlFontSize,
+ pxToRem,
+ fontFamily,
+ fontSize,
+ fontWeightLight,
+ fontWeightRegular,
+ fontWeightMedium,
+ fontWeightBold,
+ ...variants,
+ },
+ // other,
+ {
+ clone: false, // No need to clone deep
+ },
+ );
+ }
+
+ /////////////////////
+ ///////////////////// MIXINS
+ /////////////////////
+ // function createMixins(breakpoints: any, spacing: any, mixins: any) {
+ // return {
+ // toolbar: {
+ // minHeight: 56,
+ // [`${breakpoints.up("xs")} and (orientation: landscape)`]: {
+ // minHeight: 48,
+ // },
+ // [breakpoints.up("sm")]: {
+ // minHeight: 64,
+ // },
+ // },
+ // ...mixins,
+ // };
+ // }
+
+ /////////////////////
+ ///////////////////// TRANSITION
+ /////////////////////
+ function formatMs(milliseconds: number) {
+ return `${Math.round(milliseconds)}ms`;
+ }
+
+ function getAutoHeightDuration(height: number) {
+ if (!height) {
+ return 0;
+ }
+
+ const constant = height / 36;
+
+ // https://www.wolframalpha.com/input/?i=(4+%2B+15+*+(x+%2F+36+)+**+0.25+%2B+(x+%2F+36)+%2F+5)+*+10
+ return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10);
+ }
+
+ function createTransitions(inputTransitions: any) {
+ // Follow https://material.google.com/motion/duration-easing.html#duration-easing-natural-easing-curves
+ // to learn the context in which each easing should be used.
+ const easing = {
+ // This is the most common easing curve.
+ easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
+ // Objects enter the screen at full velocity from off-screen and
+ // slowly decelerate to a resting point.
+ easeOut: "cubic-bezier(0.0, 0, 0.2, 1)",
+ // Objects leave the screen at full velocity. They do not decelerate when off-screen.
+ easeIn: "cubic-bezier(0.4, 0, 1, 1)",
+ // The sharp curve is used by objects that may return to the screen at any time.
+ sharp: "cubic-bezier(0.4, 0, 0.6, 1)",
+ };
+
+ // Follow https://material.io/guidelines/motion/duration-easing.html#duration-easing-common-durations
+ // to learn when use what timing
+ const duration = {
+ shortest: 150,
+ shorter: 200,
+ short: 250,
+ // most basic recommended timing
+ standard: 300,
+ // this is to be used in complex animations
+ complex: 375,
+ // recommended when something is entering screen
+ enteringScreen: 225,
+ // recommended when something is leaving screen
+ leavingScreen: 195,
+ };
+
+ const mergedEasing = {
+ ...easing,
+ ...inputTransitions.easing,
+ };
+
+ const mergedDuration = {
+ ...duration,
+ ...inputTransitions.duration,
+ };
+
+ const create = (props = ["all"], options = {} as any) => {
+ const {
+ duration: durationOption = mergedDuration.standard,
+ easing: easingOption = mergedEasing.easeInOut,
+ delay = 0,
+ // ...other
+ } = options;
+
+ return (Array.isArray(props) ? props : [props])
+ .map(
+ (animatedProp) =>
+ `${animatedProp} ${
+ typeof durationOption === "string"
+ ? durationOption
+ : formatMs(durationOption)
+ } ${easingOption} ${
+ typeof delay === "string" ? delay : formatMs(delay)
+ }`,
+ )
+ .join(",");
+ };
+
+ return {
+ getAutoHeightDuration,
+ create,
+ ...inputTransitions,
+ easing: mergedEasing,
+ duration: mergedDuration,
+ };
+ }
+
+ /////////////////////
+ ///////////////////// PALETTE
+ /////////////////////
+ function createPalette(palette: any) {
+ // const {
+ const mode: "light" | "dark" = "light";
+ const contrastThreshold = 3;
+ const tonalOffset = 0.2;
+ // ...other
+ // } = palette;
+
+ const primary = palette.primary || getDefaultPrimary(mode);
+ const secondary = palette.secondary || getDefaultSecondary(mode);
+ const error = palette.error || getDefaultError(mode);
+ const info = palette.info || getDefaultInfo(mode);
+ const success = palette.success || getDefaultSuccess(mode);
+ const warning = palette.warning || getDefaultWarning(mode);
+
+ // Use the same logic as
+ // Bootstrap: https://github.com/twbs/bootstrap/blob/1d6e3710dd447de1a200f29e8fa521f8a0908f70/scss/_functions.scss#L59
+ // and material-components-web https://github.com/material-components/material-components-web/blob/ac46b8863c4dab9fc22c4c662dc6bd1b65dd652f/packages/mdc-theme/_functions.scss#L54
+ function getContrastText(background: string): string {
+ const contrastText =
+ getContrastRatio(background, dark.text.primary) >= contrastThreshold
+ ? dark.text.primary
+ : light.text.primary;
+
+ return contrastText;
+ }
+
+ const augmentColor = ({
+ color,
+ name,
+ mainShade = 500,
+ lightShade = 300,
+ darkShade = 700,
+ }: any) => {
+ color = { ...color };
+ if (!color.main && color[mainShade]) {
+ color.main = color[mainShade];
+ }
+
+ addLightOrDark(color, "light", lightShade, tonalOffset);
+ addLightOrDark(color, "dark", darkShade, tonalOffset);
+ if (!color.contrastText) {
+ color.contrastText = getContrastText(color.main);
+ }
+
+ return color;
+ };
+
+ const modes = { dark, light };
+
+ // if (process.env.NODE_ENV !== "production") {
+ // if (!modes[mode]) {
+ // console.error(`MUI: The palette mode \`${mode}\` is not supported.`);
+ // }
+ // }
+ const paletteOutput = deepmerge(
+ {
+ // A collection of common colors.
+ common,
+ // The palette mode, can be light or dark.
+ mode,
+ // The colors used to represent primary interface elements for a user.
+ primary: augmentColor({ color: primary, name: "primary" }),
+ // The colors used to represent secondary interface elements for a user.
+ secondary: augmentColor({
+ color: secondary,
+ name: "secondary",
+ mainShade: "A400",
+ lightShade: "A200",
+ darkShade: "A700",
+ }),
+ // The colors used to represent interface elements that the user should be made aware of.
+ error: augmentColor({ color: error, name: "error" }),
+ // The colors used to represent potentially dangerous actions or important messages.
+ warning: augmentColor({ color: warning, name: "warning" }),
+ // The colors used to present information to the user that is neutral and not necessarily important.
+ info: augmentColor({ color: info, name: "info" }),
+ // The colors used to indicate the successful completion of an action that user triggered.
+ success: augmentColor({ color: success, name: "success" }),
+ // The grey colors.
+ grey,
+ // Used by `getContrastText()` to maximize the contrast between
+ // the background and the text.
+ contrastThreshold,
+ // Takes a background color and returns the text color that maximizes the contrast.
+ getContrastText,
+ // Generate a rich color object.
+ augmentColor,
+ // Used by the functions below to shift a color's luminance by approximately
+ // two indexes within its tonal palette.
+ // E.g., shift from Red 500 to Red 300 or Red 700.
+ tonalOffset,
+ // The light and dark mode object.
+ ...modes[mode],
+ },
+ // other:
+ {},
+ );
+
+ return paletteOutput;
+ }
+
+ function addLightOrDark(
+ intent: any,
+ direction: any,
+ shade: any,
+ tonalOffset: any,
+ ): void {
+ const tonalOffsetLight = tonalOffset.light || tonalOffset;
+ const tonalOffsetDark = tonalOffset.dark || tonalOffset * 1.5;
+
+ if (!intent[direction]) {
+ if (intent.hasOwnProperty(shade)) {
+ intent[direction] = intent[shade];
+ } else if (direction === "light") {
+ intent.light = lighten(intent.main, tonalOffsetLight);
+ } else if (direction === "dark") {
+ intent.dark = darken(intent.main, tonalOffsetDark);
+ }
+ }
+ }
+
+ function getDefaultPrimary(mode = "light") {
+ if (mode === "dark") {
+ return {
+ main: blue[200],
+ light: blue[50],
+ dark: blue[400],
+ };
+ }
+ return {
+ main: blue[700],
+ light: blue[400],
+ dark: blue[800],
+ };
+ }
+
+ function getDefaultSecondary(mode = "light") {
+ if (mode === "dark") {
+ return {
+ main: grey[200],
+ light: grey[50],
+ dark: grey[400],
+ };
+ }
+ return {
+ main: grey[300],
+ light: grey[100],
+ dark: grey[600],
+ };
+ }
+
+ function getDefaultError(mode = "light") {
+ if (mode === "dark") {
+ return {
+ main: red[500],
+ light: red[300],
+ dark: red[700],
+ };
+ }
+ return {
+ main: red[700],
+ light: red[400],
+ dark: red[800],
+ };
+ }
+
+ function getDefaultInfo(mode = "light") {
+ if (mode === "dark") {
+ return {
+ main: lightBlue[400],
+ light: lightBlue[300],
+ dark: lightBlue[700],
+ };
+ }
+ return {
+ main: lightBlue[700],
+ light: lightBlue[500],
+ dark: lightBlue[900],
+ };
+ }
+
+ function getDefaultSuccess(mode = "light") {
+ if (mode === "dark") {
+ return {
+ main: green[400],
+ light: green[300],
+ dark: green[700],
+ };
+ }
+ return {
+ main: green[800],
+ light: green[500],
+ dark: green[900],
+ };
+ }
+
+ function getDefaultWarning(mode = "light") {
+ if (mode === "dark") {
+ return {
+ main: orange[400],
+ light: orange[300],
+ dark: orange[700],
+ };
+ }
+ return {
+ main: "#ed6c02",
+ light: orange[500],
+ dark: orange[900],
+ };
+ }
+
+ /////////////////////
+ ///////////////////// DEEP MERGE
+ /////////////////////
+ function isPlainObject(item: unknown): item is Record<keyof any, unknown> {
+ return (
+ item !== null && typeof item === "object" && item.constructor === Object
+ );
+ }
+
+ interface DeepmergeOptions {
+ clone?: boolean;
+ }
+
+ function deepmerge<T>(
+ target: T,
+ source: unknown,
+ options: DeepmergeOptions = { clone: true },
+ ): T {
+ const output = options.clone ? { ...target } : target;
+
+ if (isPlainObject(target) && isPlainObject(source)) {
+ Object.keys(source).forEach((key) => {
+ // Avoid prototype pollution
+ if (key === "__proto__") {
+ return;
+ }
+
+ if (
+ isPlainObject(source[key]) &&
+ key in target &&
+ isPlainObject(target[key])
+ ) {
+ // Since `output` is a clone of `target` and we have narrowed `target` in this block we can cast to the same type.
+ (output as Record<keyof any, unknown>)[key] = deepmerge(
+ target[key],
+ source[key],
+ options,
+ );
+ } else {
+ (output as Record<keyof any, unknown>)[key] = source[key];
+ }
+ });
+ }
+
+ return output;
+ }
+ return {
+ typography,
+ palette,
+ shadows,
+ shape,
+ transitions,
+ breakpoints,
+ spacing,
+ pxToRem,
+ zIndex,
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts
new file mode 100644
index 000000000..3c116fab2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/platform/api.ts
@@ -0,0 +1,337 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ CoreApiResponse,
+ TalerUri,
+ WalletNotification,
+ WalletRunConfig,
+} from "@gnu-taler/taler-util";
+import { WalletOperations } from "@gnu-taler/taler-wallet-core";
+import {
+ ExtensionOperations,
+ MessageFromExtension,
+} from "../taler-wallet-interaction-loader.js";
+import { BackgroundOperations } from "../wxApi.js";
+
+export interface Permissions {
+ /**
+ * List of named permissions.
+ */
+ permissions?: string[] | undefined;
+ /**
+ * List of origin permissions. Anything listed here must be a subset of a
+ * host that appears in the optional_permissions list in the manifest.
+ *
+ */
+ origins?: string[] | undefined;
+}
+
+/**
+ * Compatibility API that works on multiple browsers.
+ */
+export interface CrossBrowserPermissionsApi {
+ containsClipboardPermissions(): Promise<boolean>;
+ requestClipboardPermissions(): Promise<boolean>;
+ removeClipboardPermissions(): Promise<boolean>;
+}
+
+export enum ExtensionNotificationType {
+ SettingsChange = "settings-change",
+ ClearNotifications = "clear-notifications",
+}
+
+export interface SettingsChangeNotification {
+ type: ExtensionNotificationType.SettingsChange;
+
+ currentValue: Settings;
+}
+export interface ClearNotificaitonNotification {
+ type: ExtensionNotificationType.ClearNotifications;
+}
+
+export type ExtensionNotification =
+ | SettingsChangeNotification
+ | ClearNotificaitonNotification;
+
+export type MessageFromBackend =
+ | {
+ type: "wallet";
+ notification: WalletNotification;
+ }
+ | {
+ type: "web-extension";
+ notification: ExtensionNotification;
+ };
+
+export type MessageFromFrontend<
+ Op extends BackgroundOperations | WalletOperations | ExtensionOperations,
+> = Op extends BackgroundOperations
+ ? MessageFromFrontendBackground<keyof BackgroundOperations>
+ : Op extends ExtensionOperations
+ ? MessageFromExtension<keyof ExtensionOperations>
+ : Op extends WalletOperations
+ ? MessageFromFrontendWallet<keyof WalletOperations>
+ : never;
+
+export type MessageFromFrontendBackground<
+ Op extends keyof BackgroundOperations,
+> = {
+ channel: "background";
+ operation: Op;
+ payload: BackgroundOperations[Op]["request"];
+};
+
+export type MessageFromFrontendWallet<Op extends keyof WalletOperations> = {
+ channel: "wallet";
+ operation: Op;
+ payload: WalletOperations[Op]["request"];
+};
+
+export type MessageResponse = CoreApiResponse;
+
+export interface WalletWebExVersion {
+ version_name?: string | undefined;
+ version: string;
+}
+
+type F = WalletRunConfig["features"];
+type WebexWalletConfig = {
+ [P in keyof F as `wallet${Capitalize<P>}`]: F[P];
+};
+
+export interface Settings extends WebexWalletConfig {
+ injectTalerSupport: boolean;
+ autoOpen: boolean;
+ advancedMode: boolean;
+ backup: boolean;
+ langSelector: boolean;
+ showJsonOnError: boolean;
+ extendedAccountTypes: boolean;
+ showRefeshTransactions: boolean;
+ suspendIndividualTransaction: boolean;
+ showExchangeManagement: boolean;
+ selectTosFormat: boolean;
+ showWalletActivity: boolean;
+}
+
+export const defaultSettings: Settings = {
+ injectTalerSupport: true,
+ autoOpen: true,
+ advancedMode: false,
+ backup: false,
+ langSelector: false,
+ showRefeshTransactions: false,
+ suspendIndividualTransaction: false,
+ showJsonOnError: false,
+ extendedAccountTypes: false,
+ showExchangeManagement: false,
+ walletAllowHttp: false,
+ selectTosFormat: false,
+ showWalletActivity: false,
+};
+
+/**
+ * Compatibility helpers needed for browsers that don't implement
+ * WebExtension APIs consistently.
+ */
+export interface BackgroundPlatformAPI {
+ /**
+ *
+ */
+ getSettingsFromStorage(): Promise<Settings>;
+ /**
+ * Guarantee that the service workers don't die
+ */
+ keepAlive(cb: VoidFunction): void;
+ /**
+ * FIXME: should not be needed
+ *
+ * check if the platform is firefox
+ */
+ isFirefox(): boolean;
+
+ registerOnInstalled(callback: () => void): void;
+
+ /**
+ *
+ * Check if background process run as service worker. This is used from the
+ * wallet use different http api and crypto worker.
+ */
+ useServiceWorkerAsBackgroundProcess(): boolean;
+ /**
+ *
+ * Open a page into the wallet UI
+ * @param page
+ */
+ openWalletPage(page: string): void;
+ /**
+ *
+ * Register a callback to be called when the wallet is ready to start
+ * @param callback
+ */
+ notifyWhenAppIsReady(): Promise<void>;
+
+ /**
+ * Get the wallet version from manifest
+ */
+ getWalletWebExVersion(): WalletWebExVersion;
+ /**
+ * Backend API
+ */
+ registerAllIncomingConnections(): void;
+ /**
+ * Backend API
+ */
+ registerReloadOnNewVersion(): void;
+
+ /**
+ * Permission API for checking and add a listener
+ */
+ getPermissionsApi(): CrossBrowserPermissionsApi;
+ /**
+ * Used by the wallet backend to send notification about new information
+ * @param message
+ */
+ sendMessageToAllChannels(message: MessageFromBackend): void;
+
+ /**
+ * Backend API
+ *
+ * When a tab has been detected to have a Taler action the background process
+ * can use this function to redirect the tab to the wallet UI
+ *
+ * @param tabId
+ * @param page
+ */
+ redirectTabToWalletPage(tabId: number, page: string): void;
+ /**
+ * Use by the wallet backend to receive operations from frontend (popup & wallet)
+ * and send a response back.
+ *
+ * @param onNewMessage
+ */
+ listenToAllChannels(
+ notifyNewMessage: <Op extends WalletOperations | BackgroundOperations>(
+ message: MessageFromFrontend<Op> & { id: string },
+ ) => Promise<MessageResponse>,
+ ): void;
+
+ /**
+ * Change web extension Icon
+ */
+ setAlertedIcon(): void;
+ setNormalIcon(): void;
+}
+
+export interface ForegroundPlatformAPI {
+ /**
+ * Check if the extension is running under
+ * chrome incognito or firefox private mode.
+ */
+ runningOnPrivateMode(): boolean;
+ /**
+ * FIXME: should not be needed
+ *
+ * check if the platform is firefox
+ */
+ isFirefox(): boolean;
+
+ /**
+ * Permission API for checking and add a listener
+ */
+ getPermissionsApi(): CrossBrowserPermissionsApi;
+
+ /**
+ * Popup API
+ *
+ * Used when an TalerURI is found and open up from the popup UI.
+ * Closes the popup and open the URI into the wallet UI.
+ *
+ * @param talerUri
+ */
+ openWalletURIFromPopup(talerUri: TalerUri): void;
+
+ /**
+ * Popup API
+ *
+ * Open a page into the wallet UI and close the popup
+ * @param page
+ */
+ openWalletPageFromPopup(page: string): void;
+
+ /**
+ * Open a page and close the popup
+ * @param url
+ */
+ openNewURLFromPopup(url: URL): void;
+ /**
+ * Get the wallet version from manifest
+ */
+ getWalletWebExVersion(): WalletWebExVersion;
+
+ /**
+ * Popup API
+ *
+ * Read the current tab html and try to find any Taler URI or QR code present.
+ *
+ * @return Taler URI if found
+ */
+ findTalerUriInActiveTab(): Promise<string | undefined>;
+
+ /**
+ * Popup API
+ *
+ * Read the current tab html and try to find any Taler URI or QR code present.
+ *
+ * @return Taler URI if found
+ */
+ findTalerUriInClipboard(): Promise<string | undefined>;
+
+ /**
+ * Used from the frontend to send commands to the wallet
+ *
+ * @param operation
+ * @param payload
+ *
+ * @return response from the backend
+ */
+ sendMessageToBackground<Op extends WalletOperations | BackgroundOperations>(
+ message: MessageFromFrontend<Op>,
+ ): Promise<MessageResponse>;
+
+ /**
+ * Used by the wallet frontend to send notification about new information
+ * @param message
+ */
+ triggerWalletEvent(message: MessageFromBackend): void;
+
+ /**
+ * Used from the frontend to receive notifications about new information
+ * @param listener
+ * @return function to unsubscribe the listener
+ */
+ listenToWalletBackground(
+ listener: (message: MessageFromBackend) => void,
+ ): () => void;
+
+ /**
+ * Notify when platform went offline
+ */
+ listenNetworkConnectionState(
+ listener: (state: "on" | "off") => void,
+ ): () => void;
+}
diff --git a/packages/taler-wallet-webextension/src/platform/background.ts b/packages/taler-wallet-webextension/src/platform/background.ts
new file mode 100644
index 000000000..13808af2b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/platform/background.ts
@@ -0,0 +1,23 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { BackgroundPlatformAPI } from "./api.js";
+
+// it should never be undefined :)
+export let platform: BackgroundPlatformAPI = undefined!;
+export function setupPlatform(impl: BackgroundPlatformAPI): void {
+ platform = impl;
+}
diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts
new file mode 100644
index 000000000..e63040f5c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -0,0 +1,746 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Logger,
+ TalerError,
+ TalerErrorCode,
+ TalerUri,
+ TalerUriAction,
+ stringifyTalerUri,
+} from "@gnu-taler/taler-util";
+import { WalletOperations } from "@gnu-taler/taler-wallet-core";
+import { BackgroundOperations } from "../wxApi.js";
+import {
+ BackgroundPlatformAPI,
+ CrossBrowserPermissionsApi,
+ ExtensionNotificationType,
+ ForegroundPlatformAPI,
+ MessageFromBackend,
+ MessageFromFrontend,
+ MessageResponse,
+ Settings,
+ defaultSettings,
+} from "./api.js";
+
+const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
+ isFirefox,
+ getSettingsFromStorage,
+ findTalerUriInActiveTab,
+ findTalerUriInClipboard,
+ getPermissionsApi,
+ runningOnPrivateMode,
+ getWalletWebExVersion,
+ triggerWalletEvent,
+ listenToWalletBackground,
+ notifyWhenAppIsReady,
+ openWalletPage,
+ openWalletPageFromPopup,
+ openWalletURIFromPopup,
+ redirectTabToWalletPage,
+ registerAllIncomingConnections,
+ registerOnInstalled,
+ listenToAllChannels ,
+ registerReloadOnNewVersion,
+ sendMessageToAllChannels,
+ openNewURLFromPopup,
+ sendMessageToBackground,
+ useServiceWorkerAsBackgroundProcess,
+ keepAlive,
+ listenNetworkConnectionState,
+ setAlertedIcon,
+ setNormalIcon,
+};
+
+export default api;
+
+const logger = new Logger("chrome.ts");
+
+const WALLET_STORAGE_KEY = "wallet-settings";
+
+function jsonParseOrDefault(unparsed: string, def: unknown) {
+ if (!unparsed) return def;
+ try {
+ return JSON.parse(unparsed);
+ } catch (e) {
+ return def;
+ }
+}
+
+async function getSettingsFromStorage(): Promise<Settings> {
+ const data = await chrome.storage.local.get(WALLET_STORAGE_KEY);
+ if (!data) return defaultSettings;
+ const settings = data[WALLET_STORAGE_KEY];
+ return jsonParseOrDefault(settings, defaultSettings);
+}
+
+function keepAlive(callback: () => void): void {
+ if (extensionIsManifestV3()) {
+ chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
+
+ chrome.alarms.onAlarm.addListener((a) => {
+ logger.trace(`kee p alive alarm: ${a.name}`);
+ // callback()
+ });
+ // } else {
+ }
+ callback();
+}
+
+function isFirefox(): boolean {
+ return false;
+}
+
+export function containsClipboardPermissions(): Promise<boolean> {
+ return new Promise((res) => {
+ res(false);
+ // chrome.permissions.contains({ permissions: ["clipboardRead"] }, (resp) => {
+ // const le = chrome.runtime.lastError?.message;
+ // if (le) {
+ // rej(le);
+ // }
+ // res(resp);
+ // });
+ });
+}
+
+export async function requestClipboardPermissions(): Promise<boolean> {
+ return new Promise((res) => {
+ res(false);
+ // chrome.permissions.request({ permissions: ["clipboardRead"] }, (resp) => {
+ // const le = chrome.runtime.lastError?.message;
+ // if (le) {
+ // rej(le);
+ // }
+ // res(resp);
+ // });
+ });
+}
+
+export function removeClipboardPermissions(): Promise<boolean> {
+ return new Promise((res) => {
+ res(true);
+ // chrome.permissions.remove({ permissions: ["clipboardRead"] }, (resp) => {
+ // const le = chrome.runtime.lastError?.message;
+ // if (le) {
+ // rej(le);
+ // }
+ // res(resp);
+ // });
+ });
+}
+
+function getPermissionsApi(): CrossBrowserPermissionsApi {
+ return {
+ requestClipboardPermissions,
+ removeClipboardPermissions,
+ containsClipboardPermissions,
+ };
+}
+
+/**
+ *
+ * @param callback function to be called
+ */
+function notifyWhenAppIsReady(): Promise<void> {
+ return new Promise((resolve) => {
+ if (extensionIsManifestV3()) {
+ resolve();
+ } else {
+ window.addEventListener("load", () => {
+ resolve();
+ });
+ }
+ });
+}
+
+function openWalletURIFromPopup(uri: TalerUri): void {
+ const talerUri = stringifyTalerUri(uri);
+ //FIXME: this should redirect to just one place
+ // the target pathname should handle what happens if the endpoint is not there
+ // like "trying to open from popup but this uri is not handled"
+
+ let url: string | undefined = undefined;
+ switch (uri.type) {
+ case TalerUriAction.WithdrawExchange:
+ case TalerUriAction.Withdraw:
+ url = chrome.runtime.getURL(
+ `static/wallet.html#/cta/withdraw?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
+ );
+ break;
+ case TalerUriAction.Restore:
+ url = chrome.runtime.getURL(
+ `static/wallet.html#/cta/recovery?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
+ );
+ break;
+ case TalerUriAction.Pay:
+ url = chrome.runtime.getURL(
+ `static/wallet.html#/cta/pay?talerUri=${encodeURIComponent(talerUri)}`,
+ );
+ break;
+ case TalerUriAction.Refund:
+ url = chrome.runtime.getURL(
+ `static/wallet.html#/cta/refund?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
+ );
+ break;
+ case TalerUriAction.PayPull:
+ url = chrome.runtime.getURL(
+ `static/wallet.html#/cta/invoice/pay?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
+ );
+ break;
+ case TalerUriAction.PayPush:
+ url = chrome.runtime.getURL(
+ `static/wallet.html#/cta/transfer/pickup?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
+ );
+ break;
+ case TalerUriAction.PayTemplate:
+ url = chrome.runtime.getURL(
+ `static/wallet.html#/cta/pay/template?talerUri=${encodeURIComponent(
+ talerUri,
+ )}`,
+ );
+ 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;
+ default: {
+ const error: never = uri;
+ logger.warn(
+ `Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`,
+ );
+ return;
+ }
+ }
+
+ chrome.tabs.update({ active: true, url }, () => {
+ window.close();
+ });
+}
+
+function openWalletPage(page: string): void {
+ const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
+ chrome.tabs.create({ active: true, url });
+}
+
+function openWalletPageFromPopup(page: string): void {
+ const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
+ chrome.tabs.create({ active: true, url }, () => {
+ window.close();
+ });
+}
+function openNewURLFromPopup(url: URL): void {
+ // const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
+ chrome.tabs.create({ active: true, url: url.href }, () => {
+ window.close();
+ });
+}
+
+let nextMessageIndex = 0;
+
+/**
+ * To be used by the foreground
+ * @param message
+ * @returns
+ */
+async function sendMessageToBackground<
+ Op extends WalletOperations | BackgroundOperations,
+>(message: MessageFromFrontend<Op>): Promise<MessageResponse> {
+ nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);
+ const messageWithId = { ...message, id: `id_${nextMessageIndex}` };
+
+ return new Promise<MessageResponse>((resolve, reject) => {
+ logger.trace("send operation to the wallet background", message);
+ let timedout = false;
+ const timerId = setTimeout(() => {
+ timedout = true;
+ reject(TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {
+ requestMethod: "wallet",
+ requestUrl: message.operation,
+ timeoutMs: 20 * 1000,
+ }));
+ }, 20 * 1000);
+ chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
+ if (timedout) {
+ return false; //already rejected
+ }
+ clearTimeout(timerId);
+ if (chrome.runtime.lastError) {
+ reject(chrome.runtime.lastError.message);
+ } else {
+ resolve(backgroundResponse);
+ }
+ // return true to keep the channel open
+ return true;
+ });
+ });
+}
+
+/**
+ * To be used by the foreground
+ */
+let notificationPort: chrome.runtime.Port | undefined;
+function listenToWalletBackground(listener: (message: MessageFromBackend) => void): () => void {
+ if (notificationPort === undefined) {
+ notificationPort = chrome.runtime.connect({ name: "notifications" });
+ }
+ notificationPort.onMessage.addListener(listener);
+ function removeListener(): void {
+ if (notificationPort !== undefined) {
+ notificationPort.onMessage.removeListener(listener);
+ }
+ }
+ return removeListener;
+}
+
+const allPorts: chrome.runtime.Port[] = [];
+
+function triggerWalletEvent(message: MessageFromBackend): void {
+ for (const notif of allPorts) {
+ // const message: MessageFromBackend = { type: msg.type };
+ try {
+ notif.postMessage(message);
+ } catch (e) {
+ logger.error("error posting a message", e);
+ }
+ }
+}
+
+function sendMessageToAllChannels(message: MessageFromBackend): void {
+ for (const notif of allPorts) {
+ // const message: MessageFromBackend = { type: msg.type };
+ try {
+ notif.postMessage(message);
+ } catch (e) {
+ logger.error("error posting a message", e);
+ }
+ }
+}
+
+function registerAllIncomingConnections(): void {
+ chrome.runtime.onConnect.addListener((port) => {
+ try {
+ allPorts.push(port);
+ port.onDisconnect.addListener((discoPort) => {
+ try {
+ const idx = allPorts.indexOf(discoPort);
+ if (idx >= 0) {
+ allPorts.splice(idx, 1);
+ }
+ } catch (e) {
+ logger.error("error trying to remove connection", e);
+ }
+ });
+ } catch (e) {
+ logger.error("error trying to save incoming connection", e);
+ }
+ });
+ chrome.storage.onChanged.addListener((event) => {
+ if (event[WALLET_STORAGE_KEY]) {
+ sendMessageToAllChannels({
+ type: "web-extension",
+ notification: {
+ type: ExtensionNotificationType.SettingsChange,
+ currentValue: jsonParseOrDefault(
+ event[WALLET_STORAGE_KEY].newValue,
+ defaultSettings,
+ ),
+ },
+ });
+ }
+ });
+}
+
+function listenToAllChannels(
+ notifyNewMessage: <Op extends WalletOperations | BackgroundOperations>(
+ message: MessageFromFrontend<Op> & { id: string },
+ ) => Promise<MessageResponse>,
+): void {
+ chrome.runtime.onMessage.addListener((message, sender, reply) => {
+ notifyNewMessage(message)
+ .then((apiResponse) => {
+ try {
+ reply(apiResponse);
+ } catch (e) {
+ logger.error(
+ "sending response to frontend failed",
+ message,
+ apiResponse,
+ e,
+ );
+ }
+ })
+ .catch((e) => {
+ logger.error("notify to background failed", e);
+ });
+
+ // keep the connection open
+ return true;
+ });
+}
+
+function registerReloadOnNewVersion(): void {
+ // Explicitly unload the extension page as soon as an update is available,
+ // so the update gets installed as soon as possible.
+ chrome.runtime.onUpdateAvailable.addListener((details) => {
+ logger.info("update available:", details);
+ chrome.runtime.reload();
+ });
+}
+
+// async function redirectCurrentTabToWalletPage(page: string): Promise<void> {
+// let queryOptions = { active: true, lastFocusedWindow: true };
+// let [tab] = await chrome.tabs.query(queryOptions);
+
+// return redirectTabToWalletPage(tab.id!, page);
+// }
+
+async function redirectTabToWalletPage(
+ tabId: number,
+ page: string,
+): Promise<void> {
+ const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
+ logger.trace("redirecting tabId: ", tabId, " to: ", url);
+ await chrome.tabs.update(tabId, { url });
+}
+
+interface WalletVersion {
+ version_name?: string | undefined;
+ version: string;
+}
+
+function getWalletWebExVersion(): WalletVersion {
+ const manifestData = chrome.runtime.getManifest();
+ return manifestData;
+}
+
+const alertIcons = {
+ "16": "/static/img/taler-alert-16.png",
+ "19": "/static/img/taler-alert-19.png",
+ "32": "/static/img/taler-alert-32.png",
+ "38": "/static/img/taler-alert-38.png",
+ "48": "/static/img/taler-alert-48.png",
+ "64": "/static/img/taler-alert-64.png",
+ "128": "/static/img/taler-alert-128.png",
+ "256": "/static/img/taler-alert-256.png",
+ "512": "/static/img/taler-alert-512.png",
+};
+const normalIcons = {
+ "16": "/static/img/taler-logo-16.png",
+ "19": "/static/img/taler-logo-19.png",
+ "32": "/static/img/taler-logo-32.png",
+ "38": "/static/img/taler-logo-38.png",
+ "48": "/static/img/taler-logo-48.png",
+ "64": "/static/img/taler-logo-64.png",
+ "128": "/static/img/taler-logo-128.png",
+ "256": "/static/img/taler-logo-256.png",
+ "512": "/static/img/taler-logo-512.png",
+};
+function setNormalIcon(): void {
+ if (extensionIsManifestV3()) {
+ chrome.action.setIcon({ path: normalIcons });
+ } else {
+ chrome.browserAction.setIcon({ path: normalIcons });
+ }
+}
+
+function setAlertedIcon(): void {
+ if (extensionIsManifestV3()) {
+ chrome.action.setIcon({ path: alertIcons });
+ } else {
+ chrome.browserAction.setIcon({ path: alertIcons });
+ }
+}
+
+interface OffscreenCanvasRenderingContext2D
+ extends CanvasState,
+ 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;
+};
+
+interface OffscreenCanvas extends EventTarget {
+ width: number;
+ height: number;
+ getContext(
+ contextId: "2d",
+ contextAttributes?: CanvasRenderingContext2DSettings,
+ ): OffscreenCanvasRenderingContext2D | null;
+}
+declare const OffscreenCanvas: {
+ prototype: OffscreenCanvas;
+ new(width: number, height: number): OffscreenCanvas;
+};
+
+function createCanvas(size: number): OffscreenCanvas {
+ if (extensionIsManifestV3()) {
+ return new OffscreenCanvas(size, size);
+ } else {
+ const c = document.createElement("canvas");
+ c.height = size;
+ c.width = size;
+ return c;
+ }
+}
+
+async function createImage(size: number, file: string): Promise<ImageData> {
+ const r = await fetch(file);
+ const b = await r.blob();
+ const image = await createImageBitmap(b);
+ const canvas = createCanvas(size);
+ const canvasContext = canvas.getContext("2d")!;
+ canvasContext.clearRect(0, 0, canvas.width, canvas.height);
+ canvasContext.drawImage(image, 0, 0, canvas.width, canvas.height);
+ const imageData = canvasContext.getImageData(
+ 0,
+ 0,
+ canvas.width,
+ canvas.height,
+ );
+ return imageData;
+}
+
+async function registerIconChangeOnTalerContent(): Promise<void> {
+ const imgs = await Promise.all(
+ Object.entries(alertIcons).map(([key, value]) =>
+ createImage(parseInt(key, 10), value),
+ ),
+ );
+ const imageData = imgs.reduce(
+ (prev, cur) => ({ ...prev, [cur.width]: cur }),
+ {} as { [size: string]: ImageData },
+ );
+
+ if (chrome.declarativeContent) {
+ // using declarative content does not need host permission
+ // and is faster
+ const secureTalerUrlLookup = {
+ conditions: [
+ new chrome.declarativeContent.PageStateMatcher({
+ css: ["a[href^='taler://'"],
+ }),
+ ],
+ actions: [new chrome.declarativeContent.SetIcon({ imageData })],
+ };
+ const inSecureTalerUrlLookup = {
+ conditions: [
+ new chrome.declarativeContent.PageStateMatcher({
+ css: ["a[href^='taler+http://'"],
+ }),
+ ],
+ actions: [new chrome.declarativeContent.SetIcon({ imageData })],
+ };
+ chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
+ chrome.declarativeContent.onPageChanged.addRules([
+ secureTalerUrlLookup,
+ inSecureTalerUrlLookup,
+ ]);
+ });
+ return;
+ }
+
+ //this browser doesn't have declarativeContent
+ //we need host_permission and we will check the content for changing the icon
+ chrome.tabs.onUpdated.addListener(
+ async (tabId, info: chrome.tabs.TabChangeInfo) => {
+ if (tabId < 0) return;
+ if (info.status !== "complete") return;
+ const uri = await findTalerUriInTab(tabId);
+ if (uri) {
+ setAlertedIcon();
+ } else {
+ setNormalIcon();
+ }
+ },
+ );
+ chrome.tabs.onActivated.addListener(
+ async ({ tabId }: chrome.tabs.TabActiveInfo) => {
+ if (tabId < 0) return;
+ const uri = await findTalerUriInTab(tabId);
+ if (uri) {
+ setAlertedIcon();
+ } else {
+ setNormalIcon();
+ }
+ },
+ );
+}
+
+function registerOnInstalled(callback: () => void): void {
+ // This needs to be outside of main, as Firefox won't fire the event if
+ // the listener isn't created synchronously on loading the backend.
+ chrome.runtime.onInstalled.addListener(async (details) => {
+ logger.info(`onInstalled with reason: "${details.reason}"`);
+ if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
+ callback();
+ }
+ await registerIconChangeOnTalerContent();
+ });
+}
+
+function extensionIsManifestV3(): boolean {
+ return chrome.runtime.getManifest().manifest_version === 3;
+}
+
+function useServiceWorkerAsBackgroundProcess(): boolean {
+ return extensionIsManifestV3();
+}
+
+function searchForTalerLinks(): string | undefined {
+ let found;
+ found = document.querySelector("a[href^='taler://'");
+ if (found) return found.toString();
+ found = document.querySelector("a[href^='taler+http://'");
+ if (found) return found.toString();
+ return undefined;
+}
+
+async function getCurrentTab(): Promise<chrome.tabs.Tab> {
+ const queryOptions = { active: true, currentWindow: true };
+ return new Promise<chrome.tabs.Tab>((resolve, reject) => {
+ chrome.tabs.query(queryOptions, (tabs) => {
+ if (chrome.runtime.lastError) {
+ reject(chrome.runtime.lastError);
+ return;
+ }
+ resolve(tabs[0]);
+ });
+ });
+}
+
+async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
+ if (extensionIsManifestV3()) {
+ // manifest v3
+ try {
+ const res = await chrome.scripting.executeScript({
+ target: { tabId, allFrames: true },
+ func: searchForTalerLinks,
+ args: [],
+ });
+ return res[0].result;
+ } catch (e) {
+ return;
+ }
+ } else {
+ return new Promise((resolve) => {
+ //manifest v2
+ chrome.tabs.executeScript(
+ tabId,
+ {
+ code: `
+ (() => {
+ let x = document.querySelector("a[href^='taler://'") || document.querySelector("a[href^='taler+http://'");
+ return x ? x.href.toString() : null;
+ })();
+ `,
+ allFrames: false,
+ },
+ (result) => {
+ if (chrome.runtime.lastError) {
+ logger.error(JSON.stringify(chrome.runtime.lastError));
+ resolve(undefined);
+ return;
+ }
+ resolve(result[0]);
+ },
+ );
+ });
+ }
+}
+
+// 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 {
+ // //It looks like clipboard promise does not return, so we need a timeout
+ // const textInClipboard = await Promise.any([
+ // timeout(100),
+ // window.navigator.clipboard.readText(),
+ // ]);
+ // if (!textInClipboard) return;
+ // return textInClipboard.startsWith("taler://") ||
+ // textInClipboard.startsWith("taler+http://")
+ // ? textInClipboard
+ // : undefined;
+ // } catch (e) {
+ // logger.error("could not read clipboard", e);
+ // return undefined;
+ // }
+ return undefined;
+}
+
+async function findTalerUriInActiveTab(): Promise<string | undefined> {
+ const tab = await getCurrentTab();
+ if (!tab || tab.id === undefined) return;
+ return findTalerUriInTab(tab.id);
+}
+
+function listenNetworkConnectionState(
+ notify: (state: "on" | "off") => void,
+): () => void {
+ function notifyOffline() {
+ notify("off");
+ }
+ function notifyOnline() {
+ notify("on");
+ }
+ notify(window.navigator.onLine ? "on" : "off");
+ window.addEventListener("offline", notifyOffline);
+ window.addEventListener("online", notifyOnline);
+ return () => {
+ window.removeEventListener("offline", notifyOffline);
+ window.removeEventListener("online", notifyOnline);
+ };
+}
+
+function runningOnPrivateMode(): boolean {
+ return chrome.extension.inIncognitoContext;
+}
diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts
new file mode 100644
index 000000000..d6e743147
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/platform/dev.ts
@@ -0,0 +1,218 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Logger, TalerUri } from "@gnu-taler/taler-util";
+import { WalletOperations } from "@gnu-taler/taler-wallet-core";
+import { BackgroundOperations } from "../wxApi.js";
+import {
+ BackgroundPlatformAPI,
+ ForegroundPlatformAPI,
+ MessageFromBackend,
+ MessageFromFrontend,
+ MessageResponse,
+ defaultSettings,
+} from "./api.js";
+
+const logger = new Logger("dev.ts");
+
+const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
+ runningOnPrivateMode: () => false,
+ isFirefox: () => false,
+ getSettingsFromStorage: () => Promise.resolve(defaultSettings),
+ keepAlive: (cb: VoidFunction) => cb(),
+ findTalerUriInActiveTab: async () => undefined,
+ findTalerUriInClipboard: async () => undefined,
+ listenNetworkConnectionState,
+ openNewURLFromPopup: () => undefined,
+ triggerWalletEvent: () => undefined,
+ setAlertedIcon: () => undefined,
+ setNormalIcon : () => undefined,
+ getPermissionsApi: () => ({
+ containsClipboardPermissions: async () => true,
+ removeClipboardPermissions: async () => false,
+ requestClipboardPermissions: async () => false,
+ }),
+
+ getWalletWebExVersion: () => ({
+ version: "none",
+ }),
+ notifyWhenAppIsReady: () => {
+ const knownFrames = ["popup", "wallet"];
+ let total = knownFrames.length;
+ return new Promise((fn) => {
+ function waitAndNotify(): void {
+ total--;
+ logger.trace(`waitAndNotify ${total}`);
+ if (total < 1) {
+ fn();
+ }
+ }
+ knownFrames.forEach((f) => {
+ const theFrame = window.frames[f as any];
+ if (theFrame.location.href === "about:blank") {
+ waitAndNotify();
+ } else {
+ theFrame.addEventListener("load", waitAndNotify);
+ }
+ });
+ });
+ },
+
+ openWalletPage: (page: string) => {
+ // @ts-ignore
+ window.parent.redirectWallet(`wallet.html#${page}`);
+ },
+ openWalletPageFromPopup: (page: string) => {
+ // @ts-ignore
+ window.parent.redirectWallet(`wallet.html#${page}`);
+ // close the popup
+ // @ts-ignore
+ window.parent.closePopup();
+ },
+ openWalletURIFromPopup: (page: TalerUri) => {
+ alert("openWalletURIFromPopup not implemented yet");
+ },
+ redirectTabToWalletPage: (tabId: number, page: string) => {
+ alert("redirectTabToWalletPage not implemented yet");
+ },
+ registerAllIncomingConnections: () => undefined,
+ registerOnInstalled: () => Promise.resolve(),
+ registerReloadOnNewVersion: () => undefined,
+
+ useServiceWorkerAsBackgroundProcess: () => false,
+
+ listenToAllChannels: (
+ notifyNewMessage: (message: any) => Promise<MessageResponse>,
+ ) => {
+ window.addEventListener(
+ "message",
+ (event: MessageEvent<IframeMessageType>) => {
+ if (event.data.type !== "command") return;
+ const sender = event.data.header.replyMe;
+
+ notifyNewMessage(event.data.body as any).then((resp) => {
+ logger.trace(`listenToAllChannels: from ${sender}`, event);
+ if (event.source) {
+ const msg: IframeMessageResponse = {
+ type: "response",
+ header: { responseId: sender },
+ body: resp,
+ };
+ window.parent.postMessage(msg);
+ }
+ });
+ },
+ );
+ },
+ sendMessageToAllChannels: (message: MessageFromBackend) => {
+ Array.from(window.frames).forEach((w) => {
+ try {
+ w.postMessage({
+ header: {},
+ body: message,
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ },
+ listenToWalletBackground: (onNewMessage: (m: MessageFromBackend) => void) => {
+ function listener(event: MessageEvent<IframeMessageType>): void {
+ logger.trace(`listenToWalletBackground: `, event);
+ if (event.data.type !== "notification") return;
+ onNewMessage(event.data.body);
+ }
+ window.parent.addEventListener("message", listener);
+ return () => {
+ window.parent.removeEventListener("message", listener);
+ };
+ },
+
+ sendMessageToBackground: async <
+ Op extends WalletOperations | BackgroundOperations,
+ >(
+ payload: MessageFromFrontend<Op>,
+ ): Promise<MessageResponse> => {
+ const replyMe = `reply-${Math.floor(Math.random() * 100000)}`;
+ const message: IframeMessageCommand = {
+ type: "command",
+ header: { replyMe },
+ body: payload,
+ };
+
+ logger.trace(`sendMessageToBackground: `, message);
+
+ return new Promise((res, rej) => {
+ function listener(event: MessageEvent<IframeMessageType>): void {
+ if (
+ event.data.type !== "response" ||
+ event.data.header.responseId !== replyMe
+ ) {
+ return;
+ }
+ res(event.data.body);
+ window.parent.removeEventListener("message", listener);
+ }
+ window.parent.addEventListener("message", listener, {});
+ window.parent.postMessage(message);
+ });
+ },
+};
+
+type IframeMessageType =
+ | IframeMessageNotification
+ | IframeMessageResponse
+ | IframeMessageCommand;
+
+interface IframeMessageNotification {
+ type: "notification";
+ header: Record<string, never>;
+ body: MessageFromBackend;
+}
+interface IframeMessageResponse {
+ type: "response";
+ header: {
+ responseId: string;
+ };
+ body: MessageResponse;
+}
+
+interface IframeMessageCommand {
+ type: "command";
+ header: {
+ replyMe: string;
+ };
+ body: MessageFromFrontend<any>;
+}
+
+export default api;
+
+function listenNetworkConnectionState(
+ notify: (state: "on" | "off") => void,
+): () => void {
+ function notifyOffline() {
+ notify("off");
+ }
+ function notifyOnline() {
+ notify("on");
+ }
+ window.addEventListener("offline", notifyOffline);
+ window.addEventListener("online", notifyOnline);
+ return () => {
+ window.removeEventListener("offline", notifyOffline);
+ window.removeEventListener("online", notifyOnline);
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/platform/firefox.ts b/packages/taler-wallet-webextension/src/platform/firefox.ts
new file mode 100644
index 000000000..3d67423fd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/platform/firefox.ts
@@ -0,0 +1,92 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ BackgroundPlatformAPI,
+ CrossBrowserPermissionsApi,
+ ForegroundPlatformAPI,
+ Permissions,
+ Settings,
+ defaultSettings,
+} from "./api.js";
+import chromePlatform, {
+ containsClipboardPermissions as chromeClipContains,
+ removeClipboardPermissions as chromeClipRemove,
+ requestClipboardPermissions as chromeClipRequest,
+} from "./chrome.js";
+
+const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
+ ...chromePlatform,
+ isFirefox,
+ getSettingsFromStorage,
+ getPermissionsApi,
+ notifyWhenAppIsReady,
+ redirectTabToWalletPage,
+ useServiceWorkerAsBackgroundProcess,
+};
+
+export default api;
+
+function isFirefox(): boolean {
+ return true;
+}
+
+function getPermissionsApi(): CrossBrowserPermissionsApi {
+ return {
+ containsClipboardPermissions: chromeClipContains,
+ removeClipboardPermissions: chromeClipRemove,
+ requestClipboardPermissions: chromeClipRequest,
+ };
+}
+
+async function getSettingsFromStorage(): Promise<Settings> {
+ //@ts-ignore
+ const data = await browser.storage.local.get("wallet-settings");
+ if (!data) return defaultSettings;
+ const settings = data["wallet-settings"];
+ if (!settings) return defaultSettings;
+ try {
+ const parsed = JSON.parse(settings);
+ return parsed;
+ } catch (e) {
+ return defaultSettings;
+ }
+}
+
+/**
+ *
+ * @param callback function to be called
+ */
+function notifyWhenAppIsReady(): Promise<void> {
+ return new Promise((resolve) => {
+ if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) {
+ resolve();
+ } else {
+ window.addEventListener("load", () => {
+ resolve();
+ });
+ }
+ });
+}
+
+function redirectTabToWalletPage(tabId: number, page: string): void {
+ const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
+ chrome.tabs.update(tabId, { url, loadReplace: true } as any);
+}
+
+function useServiceWorkerAsBackgroundProcess(): false {
+ return false;
+}
diff --git a/packages/taler-wallet-webextension/src/platform/foreground.ts b/packages/taler-wallet-webextension/src/platform/foreground.ts
new file mode 100644
index 000000000..ae8dc8a95
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/platform/foreground.ts
@@ -0,0 +1,22 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ForegroundPlatformAPI } from "./api.js";
+
+export let platform: ForegroundPlatformAPI = undefined as any;
+export function setupPlatform(impl: ForegroundPlatformAPI): void {
+ platform = impl;
+}
diff --git a/packages/taler-wallet-webextension/src/popup/Application.tsx b/packages/taler-wallet-webextension/src/popup/Application.tsx
new file mode 100644
index 000000000..cbb9b50b2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/popup/Application.tsx
@@ -0,0 +1,229 @@
+/*
+ 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/>
+ */
+
+/**
+ * Main entry point for extension pages.
+ *
+ * @author sebasjm
+ */
+
+import {
+ TranslationProvider,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { createHashHistory } from "history";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
+import { route, Route, Router } from "preact-router";
+import { useEffect, useState } from "preact/hooks";
+import PendingTransactions from "../components/PendingTransactions.js";
+import { PopupBox } from "../components/styled/index.js";
+import { AlertProvider } from "../context/alert.js";
+import { IoCProviderForRuntime } from "../context/iocContext.js";
+import { useTalerActionURL } from "../hooks/useTalerActionURL.js";
+import { strings } from "../i18n/strings.js";
+import { Pages, PopupNavBar, PopupNavBarOptions } from "../NavigationBar.js";
+import { platform } from "../platform/foreground.js";
+import { BackupPage } from "../wallet/BackupPage.js";
+import { ProviderDetailPage } from "../wallet/ProviderDetailPage.js";
+import { BalancePage } from "./BalancePage.js";
+import { TalerActionFound } from "./TalerActionFound.js";
+
+export function Application(): VNode {
+ return (
+ <TranslationProvider source={strings}>
+ <IoCProviderForRuntime>
+ <ApplicationView />
+ </IoCProviderForRuntime>
+ </TranslationProvider>
+ );
+}
+function ApplicationView(): VNode {
+ const hash_history = createHashHistory();
+
+ const [action, setDismissed] = useTalerActionURL();
+
+ const actionUri = action?.uri;
+
+ useEffect(() => {
+ if (actionUri) {
+ route(Pages.cta({ action: encodeURIComponent(actionUri) }));
+ }
+ }, [actionUri]);
+
+ async function redirectToTxInfo(tid: string): Promise<void> {
+ redirectTo(Pages.balanceTransaction({ tid }));
+ }
+
+ function redirectToURL(str: string): void {
+ platform.openNewURLFromPopup(new URL(str))
+ }
+
+ return (
+ <Router history={hash_history}>
+ <Route
+ path={Pages.balance}
+ component={() => (
+ <PopupTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <BalancePage
+ goToWalletManualWithdraw={() => redirectTo(Pages.receiveCash({}))}
+ goToWalletDeposit={(currency: string) =>
+ redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ }
+ goToWalletHistory={(currency: string) =>
+ redirectTo(Pages.balanceHistory({ currency }))
+ }
+ />
+ </PopupTemplate>
+ )}
+ />
+
+ <Route
+ path={Pages.cta.pattern}
+ component={function Action({ action }: { action: string }) {
+ // const [, setDismissed] = useTalerActionURL();
+
+ return (
+ <PopupTemplate goToURL={redirectToURL}>
+ <TalerActionFound
+ url={decodeURIComponent(action)}
+ onDismiss={() => {
+ setDismissed(true);
+ return redirectTo(Pages.balance);
+ }}
+ />
+ </PopupTemplate>
+ );
+ }}
+ />
+
+ <Route
+ path={Pages.backup}
+ component={() => (
+ <PopupTemplate path="backup" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <BackupPage
+ onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
+ />
+ </PopupTemplate>
+ )}
+ />
+ <Route
+ path={Pages.backupProviderDetail.pattern}
+ component={({ pid }: { pid: string }) => (
+ <PopupTemplate path="backup" goToURL={redirectToURL}>
+ <ProviderDetailPage
+ onPayProvider={(uri: string) =>
+ redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+ }
+ onWithdraw={(amount: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
+ pid={pid}
+ onBack={() => redirectTo(Pages.backup)}
+ />
+ </PopupTemplate>
+ )}
+ />
+
+ <Route
+ path={Pages.balanceTransaction.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route
+ path={Pages.ctaWithdrawManual.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route
+ path={Pages.balanceDeposit.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route
+ path={Pages.balanceHistory.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route path={Pages.backupProviderAdd} component={RedirectToWalletPage} />
+ <Route
+ path={Pages.receiveCash.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route path={Pages.sendCash.pattern} component={RedirectToWalletPage} />
+ <Route path={Pages.ctaPayTemplate} component={RedirectToWalletPage} />
+ <Route path={Pages.ctaPay} component={RedirectToWalletPage} />
+ <Route path={Pages.qr} component={RedirectToWalletPage} />
+ <Route path={Pages.settings} component={RedirectToWalletPage} />
+ <Route
+ path={Pages.settingsExchangeAdd.pattern}
+ component={RedirectToWalletPage}
+ />
+ <Route path={Pages.dev} component={RedirectToWalletPage} />
+ <Route path={Pages.notifications} component={RedirectToWalletPage} />
+
+ <Route default component={Redirect} to={Pages.balance} />
+ </Router>
+ );
+}
+
+function RedirectToWalletPage(): VNode {
+ const page = (document.location.hash || "#/").replace("#", "");
+ const [showText, setShowText] = useState(false);
+ useEffect(() => {
+ platform.openWalletPageFromPopup(page);
+ setTimeout(() => {
+ setShowText(true);
+ }, 250);
+ });
+ const { i18n } = useTranslationContext();
+ if (!showText) return <Fragment />;
+ return (
+ <span>
+ <i18n.Translate>
+ this popup is being closed and you are being redirected to {page}
+ </i18n.Translate>
+ </span>
+ );
+}
+
+async function redirectTo(location: string): Promise<void> {
+ route(location);
+}
+
+function Redirect({ to }: { to: string }): null {
+ useEffect(() => {
+ route(to, true);
+ });
+ return null;
+}
+
+function PopupTemplate({
+ path,
+ children,
+ goToTransaction,
+ goToURL,
+}: {
+ path?: PopupNavBarOptions;
+ children: ComponentChildren;
+ goToTransaction?: (id: string) => Promise<void>;
+ goToURL: (s: string) => void;
+}): VNode {
+ return (
+ <Fragment>
+ <PendingTransactions goToTransaction={goToTransaction} goToURL={goToURL} />
+ <PopupNavBar path={path} />
+ <PopupBox>
+ <AlertProvider>{children}</AlertProvider>
+ </PopupBox>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx b/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx
deleted file mode 100644
index d256f6d98..000000000
--- a/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
-import { addDays } from 'date-fns';
-import { BackupView as TestedComponent } from './BackupPage';
-import { createExample } from '../test-utils';
-
-export default {
- title: 'popup/backup/list',
- component: TestedComponent,
- argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
-};
-
-
-export const LotOfProviders = createExample(TestedComponent, {
- providers: [{
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
- },
- "terms": {
- "annualFee": "ARS:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": addDays(new Date(), 13).getTime()
- }
- },
- "terms": {
- "annualFee": "ARS:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.Pending,
- },
- "terms": {
- "annualFee": "KUDOS:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.InsufficientBalance,
- },
- "terms": {
- "annualFee": "KUDOS:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.TermsChanged,
- newTerms: {
- annualFee: 'USD:2',
- storageLimitInMegabytes: 8,
- supportedProtocolVersion: '2',
- },
- oldTerms: {
- annualFee: 'USD:1',
- storageLimitInMegabytes: 16,
- supportedProtocolVersion: '1',
-
- },
- paidUntil: {
- t_ms: 'never'
- }
- },
- "terms": {
- "annualFee": "KUDOS:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.Unpaid,
- },
- "terms": {
- "annualFee": "KUDOS:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.Unpaid,
- },
- "terms": {
- "annualFee": "KUDOS:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }]
-});
-
-
-export const OneProvider = createExample(TestedComponent, {
- providers: [{
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
- },
- "terms": {
- "annualFee": "ARS:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }]
-});
-
-
-export const Empty = createExample(TestedComponent, {
- providers: []
-});
-
diff --git a/packages/taler-wallet-webextension/src/popup/BackupPage.tsx b/packages/taler-wallet-webextension/src/popup/BackupPage.tsx
deleted file mode 100644
index dcc5e5313..000000000
--- a/packages/taler-wallet-webextension/src/popup/BackupPage.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-
-import { i18n, Timestamp } from "@gnu-taler/taler-util";
-import { ProviderInfo, ProviderPaymentStatus } from "@gnu-taler/taler-wallet-core";
-import { differenceInMonths, formatDuration, intervalToDuration } from "date-fns";
-import { Fragment, JSX, VNode, h } from "preact";
-import {
- BoldLight, ButtonPrimary, ButtonSuccess, Centered,
- CenteredText, CenteredBoldText, PopupBox, RowBorderGray,
- SmallText, SmallLightText
-} from "../components/styled";
-import { useBackupStatus } from "../hooks/useBackupStatus";
-import { Pages } from "../NavigationBar";
-
-interface Props {
- onAddProvider: () => void;
-}
-
-export function BackupPage({ onAddProvider }: Props): VNode {
- const status = useBackupStatus()
- if (!status) {
- return <div>Loading...</div>
- }
- return <BackupView providers={status.providers} onAddProvider={onAddProvider} onSyncAll={status.sync} />;
-}
-
-export interface ViewProps {
- providers: ProviderInfo[],
- onAddProvider: () => void;
- onSyncAll: () => Promise<void>;
-}
-
-export function BackupView({ providers, onAddProvider, onSyncAll }: ViewProps): VNode {
- return (
- <PopupBox>
- <section>
- {providers.map((provider) => <BackupLayout
- status={provider.paymentStatus}
- timestamp={provider.lastSuccessfulBackupTimestamp}
- id={provider.syncProviderBaseUrl}
- active={provider.active}
- title={provider.name}
- />
- )}
- {!providers.length && <Centered style={{marginTop: 100}}>
- <BoldLight>No backup providers configured</BoldLight>
- <ButtonSuccess onClick={onAddProvider}><i18n.Translate>Add provider</i18n.Translate></ButtonSuccess>
- </Centered>}
- </section>
- {!!providers.length && <footer>
- <div />
- <div>
- <ButtonPrimary onClick={onSyncAll}>{
- providers.length > 1 ?
- <i18n.Translate>Sync all backups</i18n.Translate> :
- <i18n.Translate>Sync now</i18n.Translate>
- }</ButtonPrimary>
- <ButtonSuccess onClick={onAddProvider}>Add provider</ButtonSuccess>
- </div>
- </footer>}
- </PopupBox>
- )
-}
-
-interface TransactionLayoutProps {
- status: ProviderPaymentStatus;
- timestamp?: Timestamp;
- title: string;
- id: string;
- active: boolean;
-}
-
-function BackupLayout(props: TransactionLayoutProps): JSX.Element {
- const date = !props.timestamp ? undefined : new Date(props.timestamp.t_ms);
- const dateStr = date?.toLocaleString([], {
- dateStyle: "medium",
- timeStyle: "short",
- } as any);
-
-
- return (
- <RowBorderGray>
- <div style={{ color: !props.active ? "grey" : undefined }}>
- <a href={Pages.provider_detail.replace(':pid', encodeURIComponent(props.id))}><span>{props.title}</span></a>
-
- {dateStr && <SmallText style={{marginTop: 5}}>Last synced: {dateStr}</SmallText>}
- {!dateStr && <SmallLightText style={{marginTop: 5}}>Not synced</SmallLightText>}
- </div>
- <div>
- {props.status?.type === 'paid' ?
- <ExpirationText until={props.status.paidUntil} /> :
- <div>{props.status.type}</div>
- }
- </div>
- </RowBorderGray>
- );
-}
-
-function ExpirationText({ until }: { until: Timestamp }) {
- return <Fragment>
- <CenteredText> Expires in </CenteredText>
- <CenteredBoldText {...({ color: colorByTimeToExpire(until) })}> {daysUntil(until)} </CenteredBoldText>
- </Fragment>
-}
-
-function colorByTimeToExpire(d: Timestamp) {
- if (d.t_ms === 'never') return 'rgb(28, 184, 65)'
- const months = differenceInMonths(d.t_ms, new Date())
- return months > 1 ? 'rgb(28, 184, 65)' : 'rgb(223, 117, 20)';
-}
-
-function daysUntil(d: Timestamp) {
- if (d.t_ms === 'never') return undefined
- const duration = intervalToDuration({
- start: d.t_ms,
- end: new Date(),
- })
- const str = formatDuration(duration, {
- delimiter: ', ',
- format: [
- duration?.years ? 'years' : (
- duration?.months ? 'months' : (
- duration?.days ? 'days' : (
- duration.hours ? 'hours' : 'minutes'
- )
- )
- )
- ]
- })
- return `${str}`
-} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
index 382f9b549..626ad4977 100644
--- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,205 +15,229 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample, NullLink } from '../test-utils';
-import { BalanceView as TestedComponent } from './BalancePage';
+import { AmountString, ScopeType } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { BalanceView as TestedComponent } from "./BalancePage.js";
export default {
- title: 'popup/balance',
- component: TestedComponent,
- argTypes: {
- }
+ title: "balance",
};
-
-export const NotYetLoaded = createExample(TestedComponent, {
+export const EmptyBalance = tests.createExample(TestedComponent, {
+ balances: [],
+ goToWalletManualWithdraw: {},
});
-export const GotError = createExample(TestedComponent, {
- balance: {
- hasError: true,
- message: 'Network error'
- },
- Linker: NullLink,
-});
-
-export const EmptyBalance = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: []
+export const SomeCoins = tests.createExample(TestedComponent, {
+ balances: [
+ {
+ flags: [],
+ available: "USD:10.5" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
- },
- Linker: NullLink,
+ ],
+ addAction: {},
+ goToWalletManualWithdraw: {},
});
-export const SomeCoins = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: [{
- available: 'USD:10.5',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- }]
+export const SomeCoinsInTreeCurrencies = tests.createExample(TestedComponent, {
+ balances: [
+ {
+ flags: [],
+ available: "EUR:1" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
- },
- Linker: NullLink,
-});
-
-export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: [{
- available: 'USD:2.23',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:5.11',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- }]
+ {
+ flags: [],
+ available: "TESTKUDOS:2000" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
- },
- Linker: NullLink,
-});
-
-export const SomeCoinsAndOutgoingMoney = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: [{
- available: 'USD:2.23',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:5.11',
- requiresUserInput: false
- }]
+ {
+ flags: [],
+ available: "JPY:4" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "EUR:15" as AmountString,
+ pendingOutgoing: "EUR:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
- },
- Linker: NullLink,
+ ],
+ goToWalletManualWithdraw: {},
+ addAction: {},
});
-export const SomeCoinsAndMovingMoney = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: [{
- available: 'USD:2.23',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:2',
- pendingOutgoing: 'USD:5.11',
- requiresUserInput: false
- }]
+export const NoCoinsInTreeCurrencies = tests.createExample(TestedComponent, {
+ balances: [
+ {
+ flags: [],
+ available: "EUR:3" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
- },
- Linker: NullLink,
-});
-
-export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: [{
- available: 'USD:2',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:5.1',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- },{
- available: 'EUR:4',
- hasPendingTransactions: false,
- pendingIncoming: 'EUR:0',
- pendingOutgoing: 'EUR:3.01',
- requiresUserInput: false
- }]
+ {
+ flags: [],
+ available: "USD:2" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
- },
- Linker: NullLink,
-});
-
-export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: [{
- available: 'USD:1',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- },{
- available: 'COL:2000',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- },{
- available: 'EUR:4',
- hasPendingTransactions: false,
- pendingIncoming: 'EUR:15',
- pendingOutgoing: 'EUR:0',
- requiresUserInput: false
- }]
+ {
+ flags: [],
+ available: "ARS:1" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "EUR:15" as AmountString,
+ pendingOutgoing: "EUR:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
- },
- Linker: NullLink,
+ ],
+ goToWalletManualWithdraw: {},
+ addAction: {},
});
-
-export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: [{
- available: 'USD:13451',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- },{
- available: 'EUR:202.02',
- hasPendingTransactions: false,
- pendingIncoming: 'EUR:0',
- pendingOutgoing: 'EUR:0',
- requiresUserInput: false
- },{
- available: 'ARS:30',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- },{
- available: 'JPY:51223233',
- hasPendingTransactions: false,
- pendingIncoming: 'EUR:0',
- pendingOutgoing: 'EUR:0',
- requiresUserInput: false
- },{
- available: 'JPY:51223233',
- hasPendingTransactions: false,
- pendingIncoming: 'EUR:0',
- pendingOutgoing: 'EUR:0',
- requiresUserInput: false
- },{
- available: 'DEMOKUDOS:6',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- },{
- available: 'TESTKUDOS:6',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:5',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- }]
+export const SomeCoinsInFiveCurrencies = tests.createExample(TestedComponent, {
+ balances: [
+ {
+ flags: [],
+ available: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ {
+ flags: [],
+ available: "ARS:13451" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ {
+ flags: [],
+ available: "EUR:202.02" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "EUR:0" as AmountString,
+ pendingOutgoing: "EUR:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ {
+ flags: [],
+ available: "JPY:0" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "EUR:0" as AmountString,
+ pendingOutgoing: "EUR:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ {
+ flags: [],
+ available: "JPY:51223233" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "EUR:0" as AmountString,
+ pendingOutgoing: "EUR:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ {
+ flags: [],
+ available: "DEMOKUDOS:6" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ {
+ flags: [],
+ available: "TESTKUDOS:6" as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:5" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "TESTKUDOS",
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
},
- },
- Linker: NullLink,
+ ],
+ goToWalletManualWithdraw: {},
+ addAction: {},
});
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index 8e5c5c42e..93770312e 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -1,155 +1,195 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
- amountFractionalBase, Amounts,
- Balance, BalancesResponse,
- i18n
+ Amounts,
+ NotificationType,
+ WalletBalance,
} from "@gnu-taler/taler-util";
-import { JSX, h, Fragment } from "preact";
-import { ErrorMessage } from "../components/ErrorMessage";
-import { PopupBox, Centered, ButtonPrimary, ErrorBox, Middle } from "../components/styled/index";
-import { BalancesHook, useBalances } from "../hooks/useBalances";
-import { PageLink, renderAmount } from "../renderHtml";
-
-
-export function BalancePage({ goToWalletManualWithdraw }: { goToWalletManualWithdraw: () => void }) {
- const balance = useBalances()
- return <BalanceView balance={balance} Linker={PageLink} goToWalletManualWithdraw={goToWalletManualWithdraw} />
-}
-export interface BalanceViewProps {
- balance: BalancesHook;
- Linker: typeof PageLink;
- goToWalletManualWithdraw: () => void;
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { BalanceTable } from "../components/BalanceTable.js";
+import { ErrorAlertView } from "../components/CurrentAlerts.js";
+import { Loading } from "../components/Loading.js";
+import { MultiActionButton } from "../components/MultiActionButton.js";
+import {
+ ErrorAlert,
+ alertFromError,
+ useAlertContext,
+} from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { Button } from "../mui/Button.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { StateViewMap, compose } from "../utils/index.js";
+import { AddNewActionView } from "../wallet/AddNewActionView.js";
+import { NoBalanceHelp } from "./NoBalanceHelp.js";
+
+export interface Props {
+ goToWalletDeposit: (currency: string) => Promise<void>;
+ goToWalletHistory: (currency: string) => Promise<void>;
+ goToWalletManualWithdraw: () => Promise<void>;
}
-function formatPending(entry: Balance): JSX.Element {
- let incoming: JSX.Element | undefined;
- let payment: JSX.Element | undefined;
-
- const available = Amounts.parseOrThrow(entry.available);
- const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
- const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
-
- if (!Amounts.isZero(pendingIncoming)) {
- incoming = (
- <span><i18n.Translate>
- <span style={{ color: "darkgreen" }} title="incoming amount">
- {"+"}
- {renderAmount(entry.pendingIncoming)}
- </span>{" "}
- </i18n.Translate></span>
- );
+export type State = State.Loading | State.Error | State.Action | State.Balances;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
}
- if (!Amounts.isZero(pendingOutgoing)) {
- payment = (
- <span><i18n.Translate>
- <span style={{ color: "darkred" }} title="outgoing amount">
- {"-"}
- {renderAmount(entry.pendingOutgoing)}
- </span>{" "}
- </i18n.Translate></span>
- );
+
+ export interface Error {
+ status: "error";
+ error: ErrorAlert;
}
- const l = [incoming, payment].filter((x) => x !== undefined);
- if (l.length === 0) {
- return <span />;
+ export interface Action {
+ status: "action";
+ error: undefined;
+ cancel: ButtonHandler;
}
- if (l.length === 1) {
- return <span>{l}</span>;
+ export interface Balances {
+ status: "balance";
+ error: undefined;
+ balances: WalletBalance[];
+ addAction: ButtonHandler;
+ goToWalletDeposit: (currency: string) => Promise<void>;
+ goToWalletHistory: (currency: string) => Promise<void>;
+ goToWalletManualWithdraw: ButtonHandler;
}
- return (
- <span>
- {l[0]}, {l[1]}
- </span>
+}
+
+function useComponentState({
+ goToWalletDeposit,
+ goToWalletHistory,
+ goToWalletManualWithdraw,
+}: Props): State {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const { pushAlertOnError } = useAlertContext();
+ const [addingAction, setAddingAction] = useState(false);
+ const state = useAsyncAsHook(() =>
+ api.wallet.call(WalletApiOperation.GetBalances, {}),
);
+
+ useEffect(() =>
+ api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
+ state?.retry,
+ ),
+ );
+
+ if (!state) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (state.hasError) {
+ return {
+ status: "error",
+ error: alertFromError( i18n,
+ i18n.str`Could not load the balance`, state),
+ };
+ }
+ if (addingAction) {
+ return {
+ status: "action",
+ error: undefined,
+ cancel: {
+ onClick: pushAlertOnError(async () => setAddingAction(false)),
+ },
+ };
+ }
+ return {
+ status: "balance",
+ error: undefined,
+ balances: state.response.balances,
+ addAction: {
+ onClick: pushAlertOnError(async () => setAddingAction(true)),
+ },
+ goToWalletManualWithdraw: {
+ onClick: pushAlertOnError(goToWalletManualWithdraw),
+ },
+ goToWalletDeposit,
+ goToWalletHistory,
+ };
}
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ action: ActionView,
+ balance: BalanceView,
+};
+
+export const BalancePage = compose(
+ "BalancePage",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
+
+function ActionView({ cancel }: State.Action): VNode {
+ return <AddNewActionView onCancel={cancel.onClick!} />;
+}
-export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) {
-
- function Content() {
- if (!balance) {
- return <span />
- }
-
- if (balance.hasError) {
- return (<section>
- <ErrorBox>{balance.message}</ErrorBox>
- <p>
- Click <Linker pageName="welcome">here</Linker> for help and
- diagnostics.
- </p>
- </section>)
- }
- if (balance.response.balances.length === 0) {
- return (<section data-expanded>
- <Middle>
- <p><i18n.Translate>
- You have no balance to show. Need some{" "}
- <Linker pageName="/welcome">help</Linker> getting started?
- </i18n.Translate></p>
- </Middle>
- </section>)
- }
- return <section data-expanded data-centered>
- <table style={{width:'100%'}}>{balance.response.balances.map((entry) => {
- const av = Amounts.parseOrThrow(entry.available);
- // Create our number formatter.
- let formatter;
- try {
- formatter = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: av.currency,
- currencyDisplay: 'symbol'
- // These options are needed to round to whole numbers if that's what you want.
- //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
- //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
- });
- } catch {
- formatter = new Intl.NumberFormat('en-US', {
- // style: 'currency',
- // currency: av.currency,
- // These options are needed to round to whole numbers if that's what you want.
- //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
- //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
- });
- }
-
- const v = formatter.format(av.value + av.fraction / amountFractionalBase);
- const fontSize = v.length < 8 ? '3em' : (v.length < 13 ? '2em' : '1em')
- return (<tr>
- <td style={{ height: 50, fontSize, width: '60%', textAlign: 'right', padding: 0 }}>{v}</td>
- <td style={{ maxWidth: '2em', overflowX: 'hidden' }}>{av.currency}</td>
- <td style={{ fontSize: 'small', color: 'gray' }}>{formatPending(entry)}</td>
- </tr>
- );
- })}</table>
- </section>
+export function BalanceView(state: State.Balances): VNode {
+ const { i18n } = useTranslationContext();
+ const currencyWithNonZeroAmount = state.balances
+ .filter((b) => !Amounts.isZero(b.available))
+ .map((b) => {
+ b.flags
+ return b.available.split(":")[0]
+ });
+
+ if (state.balances.length === 0) {
+ return (
+ <NoBalanceHelp
+ goToWalletManualWithdraw={state.goToWalletManualWithdraw}
+ />
+ );
}
- return <PopupBox>
- {/* <section> */}
- <Content />
- {/* </section> */}
- <footer>
- <div />
- <ButtonPrimary onClick={goToWalletManualWithdraw}>Withdraw</ButtonPrimary>
- </footer>
- </PopupBox>
+ return (
+ <Fragment>
+ <section>
+ <BalanceTable
+ balances={state.balances}
+ goToWalletHistory={state.goToWalletHistory}
+ />
+ </section>
+ <footer style={{ justifyContent: "space-between" }}>
+ <Button
+ variant="contained"
+ onClick={state.goToWalletManualWithdraw.onClick}
+ >
+ <i18n.Translate>Add</i18n.Translate>
+ </Button>
+ {currencyWithNonZeroAmount.length > 0 && (
+ <MultiActionButton
+ label={(s) => i18n.str`Send ${s}`}
+ actions={currencyWithNonZeroAmount}
+ onClick={(c) => state.goToWalletDeposit(c)}
+ />
+ )}
+ </footer>
+ </Fragment>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/popup/Debug.tsx b/packages/taler-wallet-webextension/src/popup/Debug.tsx
deleted file mode 100644
index ccc747466..000000000
--- a/packages/taler-wallet-webextension/src/popup/Debug.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { JSX, h } from "preact";
-import { Diagnostics } from "../components/Diagnostics";
-import { useDiagnostics } from "../hooks/useDiagnostics.js";
-import * as wxApi from "../wxApi";
-
-
-export function DeveloperPage(props: any): JSX.Element {
- const [status, timedOut] = useDiagnostics();
- return (
- <div>
- <p>Debug tools:</p>
- <button onClick={openExtensionPage("/static/popup.html")}>wallet tab</button>
- <br />
- <button onClick={confirmReset}>reset</button>
- <Diagnostics diagnostics={status} timedOut={timedOut} />
- </div>
- );
-}
-
-export function reload(): void {
- try {
- chrome.runtime.reload();
- window.close();
- } catch (e) {
- // Functionality missing in firefox, ignore!
- }
-}
-
-export async function confirmReset(): Promise<void> {
- if (
- confirm(
- "Do you want to IRREVOCABLY DESTROY everything inside your" +
- " wallet and LOSE ALL YOUR COINS?",
- )
- ) {
- await wxApi.resetDb();
- window.close();
- }
-}
-
-export function openExtensionPage(page: string) {
- return () => {
- chrome.tabs.create({
- url: chrome.extension.getURL(page),
- });
- };
-}
-
diff --git a/packages/taler-wallet-webextension/src/popup/History.stories.tsx b/packages/taler-wallet-webextension/src/popup/History.stories.tsx
deleted file mode 100644
index daa263a81..000000000
--- a/packages/taler-wallet-webextension/src/popup/History.stories.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import {
- PaymentStatus,
- TransactionCommon, TransactionDeposit, TransactionPayment,
- TransactionRefresh, TransactionRefund, TransactionTip, TransactionType,
- TransactionWithdrawal,
- WithdrawalType
-} from '@gnu-taler/taler-util';
-import { createExample } from '../test-utils';
-import { HistoryView as TestedComponent } from './History';
-
-export default {
- title: 'popup/history/list',
- component: TestedComponent,
-};
-
-const commonTransaction = {
- amountRaw: 'USD:10',
- amountEffective: 'USD:9',
- pending: false,
- timestamp: {
- t_ms: new Date().getTime()
- },
- transactionId: '12',
-} as TransactionCommon
-
-const exampleData = {
- withdraw: {
- ...commonTransaction,
- type: TransactionType.Withdrawal,
- exchangeBaseUrl: 'http://exchange.demo.taler.net',
- withdrawalDetails: {
- confirmed: false,
- exchangePaytoUris: ['payto://x-taler-bank/bank/account'],
- type: WithdrawalType.ManualTransfer,
- }
- } as TransactionWithdrawal,
- payment: {
- ...commonTransaction,
- amountEffective: 'USD:11',
- type: TransactionType.Payment,
- info: {
- contractTermsHash: 'ASDZXCASD',
- merchant: {
- name: 'the merchant',
- },
- orderId: '2021.167-03NPY6MCYMVGT',
- products: [],
- summary: 'the summary',
- fulfillmentMessage: '',
- },
- proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
- status: PaymentStatus.Accepted,
- } as TransactionPayment,
- deposit: {
- ...commonTransaction,
- type: TransactionType.Deposit,
- depositGroupId: '#groupId',
- targetPaytoUri: 'payto://x-taler-bank/bank/account',
- } as TransactionDeposit,
- refresh: {
- ...commonTransaction,
- type: TransactionType.Refresh,
- exchangeBaseUrl: 'http://exchange.taler',
- } as TransactionRefresh,
- tip: {
- ...commonTransaction,
- type: TransactionType.Tip,
- merchantBaseUrl: 'http://merchant.taler',
- } as TransactionTip,
- refund: {
- ...commonTransaction,
- type: TransactionType.Refund,
- refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
- info: {
- contractTermsHash: 'ASDZXCASD',
- merchant: {
- name: 'the merchant',
- },
- orderId: '2021.167-03NPY6MCYMVGT',
- products: [],
- summary: 'the summary',
- fulfillmentMessage: '',
- },
- } as TransactionRefund,
-}
-
-export const EmptyWithBalance = createExample(TestedComponent, {
- list: [],
- balances: [{
- available: 'TESTKUDOS:10',
- pendingIncoming: 'TESTKUDOS:0',
- pendingOutgoing: 'TESTKUDOS:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
-});
-
-export const EmptyWithNoBalance = createExample(TestedComponent, {
- list: [],
- balances: []
-});
-
-export const One = createExample(TestedComponent, {
- list: [exampleData.withdraw],
- balances: [{
- available: 'USD:10',
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
-});
-
-export const OnePending = createExample(TestedComponent, {
- list: [{
- ...exampleData.withdraw,
- pending: true,
- }],
- balances: [{
- available: 'USD:10',
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
-});
-
-export const Several = createExample(TestedComponent, {
- list: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
- exampleData.refresh,
- exampleData.refund,
- exampleData.tip,
- exampleData.deposit,
- ],
- balances: [{
- available: 'TESTKUDOS:10',
- pendingIncoming: 'TESTKUDOS:0',
- pendingOutgoing: 'TESTKUDOS:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
-});
-
-export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
- list: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
- exampleData.refresh,
- exampleData.refund,
- exampleData.tip,
- exampleData.deposit,
- ],
- balances: [{
- available: 'TESTKUDOS:10',
- pendingIncoming: 'TESTKUDOS:0',
- pendingOutgoing: 'TESTKUDOS:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }, {
- available: 'USD:10',
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
-});
-
diff --git a/packages/taler-wallet-webextension/src/popup/History.tsx b/packages/taler-wallet-webextension/src/popup/History.tsx
deleted file mode 100644
index 1447da9b0..000000000
--- a/packages/taler-wallet-webextension/src/popup/History.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { AmountString, Balance, i18n, Transaction, TransactionsResponse } from "@gnu-taler/taler-util";
-import { h, JSX } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { PopupBox } from "../components/styled";
-import { TransactionItem } from "../components/TransactionItem";
-import { useBalances } from "../hooks/useBalances";
-import * as wxApi from "../wxApi";
-
-
-export function HistoryPage(props: any): JSX.Element {
- const [transactions, setTransactions] = useState<
- TransactionsResponse | undefined
- >(undefined);
- const balance = useBalances()
- const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || [])
-
- useEffect(() => {
- const fetchData = async (): Promise<void> => {
- const res = await wxApi.getTransactions();
- setTransactions(res);
- };
- fetchData();
- }, []);
-
- if (!transactions) {
- return <div>Loading ...</div>;
- }
-
- return <HistoryView balances={balanceWithoutError} list={[...transactions.transactions].reverse()} />;
-}
-
-function amountToString(c: AmountString) {
- const idx = c.indexOf(':')
- return `${c.substring(idx + 1)} ${c.substring(0, idx)}`
-}
-
-
-
-export function HistoryView({ list, balances }: { list: Transaction[], balances: Balance[] }) {
- const multiCurrency = balances.length > 1
- return <PopupBox noPadding>
- {balances.length > 0 && <header>
- {multiCurrency ? <div class="title">
- Balance: <ul style={{ margin: 0 }}>
- {balances.map(b => <li>{b.available}</li>)}
- </ul>
- </div> : <div class="title">
- Balance: <span>{amountToString(balances[0].available)}</span>
- </div>}
- </header>}
- {list.length === 0 ? <section data-expanded data-centered>
- <p><i18n.Translate>
- You have no history yet, here you will be able to check your last transactions.
- </i18n.Translate></p>
- </section> :
- <section>
- {list.slice(0, 3).map((tx, i) => (
- <TransactionItem key={i} tx={tx} multiCurrency={multiCurrency} />
- ))}
- </section>
- }
- <footer style={{ justifyContent: 'space-around' }}>
- {list.length > 0 &&
- <a target="_blank"
- rel="noopener noreferrer"
- style={{ color: 'darkgreen', textDecoration: 'none' }}
- href={chrome.extension ? chrome.extension.getURL(`/static/wallet.html#/history`) : '#'}>VIEW MORE TRANSACTIONS</a>
- }
- </footer>
- </PopupBox>
-}
diff --git a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
new file mode 100644
index 000000000..c698066e7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
@@ -0,0 +1,53 @@
+/*
+ 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 { css } from "@linaria/core";
+import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Alert } from "../mui/Alert.js";
+import { Button } from "../mui/Button.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { Paper } from "../mui/Paper.js";
+
+const margin = css`
+ margin: 1em;
+`;
+
+export function NoBalanceHelp({
+ goToWalletManualWithdraw,
+}: {
+ goToWalletManualWithdraw: ButtonHandler;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (<Fragment>
+
+ <Paper class={margin}>
+ <Alert title={i18n.str`Your wallet is empty.`} severity="info">
+ <Button
+ fullWidth
+ color="info"
+ variant="outlined"
+ onClick={goToWalletManualWithdraw.onClick}
+ >
+ <i18n.Translate>Get digital cash</i18n.Translate>
+ </Button>
+ </Alert>
+ </Paper>
+ <a target="_bank" rel="noreferrer" href="https://demo.taler.net/" style={{ display: "block" }}>
+ <i18n.Translate>Try the demo bank and withdraw test money.</i18n.Translate> »
+ </a>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx
deleted file mode 100644
index de1f67b96..000000000
--- a/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample } from '../test-utils';
-import { ConfirmProviderView as TestedComponent } from './ProviderAddPage';
-
-export default {
- title: 'popup/backup/confirm',
- component: TestedComponent,
- argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
-};
-
-
-export const DemoService = createExample(TestedComponent, {
- url: 'https://sync.demo.taler.net/',
- provider: {
- annual_fee: 'KUDOS:0.1',
- storage_limit_in_megabytes: 20,
- supported_protocol_version: '1'
- }
-});
-
-export const FreeService = createExample(TestedComponent, {
- url: 'https://sync.taler:9667/',
- provider: {
- annual_fee: 'ARS:0',
- storage_limit_in_megabytes: 20,
- supported_protocol_version: '1'
- }
-});
diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddPage.tsx
deleted file mode 100644
index 55686ee97..000000000
--- a/packages/taler-wallet-webextension/src/popup/ProviderAddPage.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- Amounts,
- BackupBackupProviderTerms,
- canonicalizeBaseUrl,
- i18n,
-} from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { Checkbox } from "../components/Checkbox";
-import { ErrorMessage } from "../components/ErrorMessage";
-import {
- Button,
- ButtonPrimary,
- Input,
- LightText,
- PopupBox,
- SmallLightText,
-} from "../components/styled/index";
-import * as wxApi from "../wxApi";
-
-interface Props {
- currency: string;
- onBack: () => void;
-}
-
-function getJsonIfOk(r: Response) {
- if (r.ok) {
- return r.json();
- } else {
- if (r.status >= 400 && r.status < 500) {
- throw new Error(`URL may not be right: (${r.status}) ${r.statusText}`);
- } else {
- throw new Error(
- `Try another server: (${r.status}) ${
- r.statusText || "internal server error"
- }`,
- );
- }
- }
-}
-
-export function ProviderAddPage({ onBack }: Props): VNode {
- const [verifying, setVerifying] = useState<
- | { url: string; name: string; provider: BackupBackupProviderTerms }
- | undefined
- >(undefined);
-
- async function getProviderInfo(
- url: string,
- ): Promise<BackupBackupProviderTerms> {
- return fetch(`${url}config`)
- .catch((e) => {
- throw new Error(`Network error`);
- })
- .then(getJsonIfOk);
- }
-
- if (!verifying) {
- return (
- <SetUrlView
- onCancel={onBack}
- onVerify={(url) => getProviderInfo(url)}
- onConfirm={(url, name) =>
- getProviderInfo(url)
- .then((provider) => {
- setVerifying({ url, name, provider });
- })
- .catch((e) => e.message)
- }
- />
- );
- }
- return (
- <ConfirmProviderView
- provider={verifying.provider}
- url={verifying.url}
- onCancel={() => {
- setVerifying(undefined);
- }}
- onConfirm={() => {
- wxApi.addBackupProvider(verifying.url, verifying.name).then(onBack);
- }}
- />
- );
-}
-
-export interface SetUrlViewProps {
- initialValue?: string;
- onCancel: () => void;
- onVerify: (s: string) => Promise<BackupBackupProviderTerms | undefined>;
- onConfirm: (url: string, name: string) => Promise<string | undefined>;
- withError?: string;
-}
-
-export function SetUrlView({
- initialValue,
- onCancel,
- onVerify,
- onConfirm,
- withError,
-}: SetUrlViewProps) {
- const [value, setValue] = useState<string>(initialValue || "");
- const [urlError, setUrlError] = useState(false);
- const [name, setName] = useState<string | undefined>(undefined);
- const [error, setError] = useState<string | undefined>(withError);
- useEffect(() => {
- try {
- const url = canonicalizeBaseUrl(value);
- onVerify(url)
- .then((r) => {
- setUrlError(false);
- setName(new URL(url).hostname);
- })
- .catch(() => {
- setUrlError(true);
- setName(undefined);
- });
- } catch {
- setUrlError(true);
- setName(undefined);
- }
- }, [value]);
- return (
- <PopupBox>
- <section>
- <h1> Add backup provider</h1>
- <ErrorMessage
- title={error && "Could not get provider information"}
- description={error}
- />
- <LightText> Backup providers may charge for their service</LightText>
- <p>
- <Input invalid={urlError}>
- <label>URL</label>
- <input
- type="text"
- placeholder="https://"
- value={value}
- onChange={(e) => setValue(e.currentTarget.value)}
- />
- </Input>
- <Input>
- <label>Name</label>
- <input
- type="text"
- disabled={name === undefined}
- value={name}
- onChange={(e) => setName(e.currentTarget.value)}
- />
- </Input>
- </p>
- </section>
- <footer>
- <Button onClick={onCancel}>
- <i18n.Translate> &lt; Back</i18n.Translate>
- </Button>
- <ButtonPrimary
- disabled={!value && !urlError}
- onClick={() => {
- const url = canonicalizeBaseUrl(value);
- return onConfirm(url, name!).then((r) =>
- r ? setError(r) : undefined,
- );
- }}
- >
- <i18n.Translate>Next</i18n.Translate>
- </ButtonPrimary>
- </footer>
- </PopupBox>
- );
-}
-
-export interface ConfirmProviderViewProps {
- provider: BackupBackupProviderTerms;
- url: string;
- onCancel: () => void;
- onConfirm: () => void;
-}
-export function ConfirmProviderView({
- url,
- provider,
- onCancel,
- onConfirm,
-}: ConfirmProviderViewProps) {
- const [accepted, setAccepted] = useState(false);
-
- return (
- <PopupBox>
- <section>
- <h1>Review terms of service</h1>
- <div>
- Provider URL:{" "}
- <a href={url} target="_blank">
- {url}
- </a>
- </div>
- <SmallLightText>
- Please review and accept this provider's terms of service
- </SmallLightText>
- <h2>1. Pricing</h2>
- <p>
- {Amounts.isZero(provider.annual_fee)
- ? "free of charge"
- : `${provider.annual_fee} per year of service`}
- </p>
- <h2>2. Storage</h2>
- <p>
- {provider.storage_limit_in_megabytes} megabytes of storage per year of
- service
- </p>
- <Checkbox
- label="Accept terms of service"
- name="terms"
- onToggle={() => setAccepted((old) => !old)}
- enabled={accepted}
- />
- </section>
- <footer>
- <Button onClick={onCancel}>
- <i18n.Translate> &lt; Back</i18n.Translate>
- </Button>
- <ButtonPrimary disabled={!accepted} onClick={onConfirm}>
- <i18n.Translate>Add provider</i18n.Translate>
- </ButtonPrimary>
- </footer>
- </PopupBox>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx
deleted file mode 100644
index 2daf49e0c..000000000
--- a/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample } from '../test-utils';
-import { SetUrlView as TestedComponent } from './ProviderAddPage';
-
-export default {
- title: 'popup/backup/add',
- component: TestedComponent,
- argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
-};
-
-
-export const Initial = createExample(TestedComponent, {
-});
-
-export const WithValue = createExample(TestedComponent, {
- initialValue: 'sync.demo.taler.net'
-});
-
-export const WithConnectionError = createExample(TestedComponent, {
- withError: 'Network error'
-});
-
-export const WithClientError = createExample(TestedComponent, {
- withError: 'URL may not be right: (404) Not Found'
-});
-
-export const WithServerError = createExample(TestedComponent, {
- withError: 'Try another server: (500) Internal Server Error'
-});
diff --git a/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx
deleted file mode 100644
index 4416608f8..000000000
--- a/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
-import { createExample } from '../test-utils';
-import { ProviderView as TestedComponent } from './ProviderDetailPage';
-
-export default {
- title: 'popup/backup/details',
- component: TestedComponent,
- argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
-};
-
-
-export const Active = createExample(TestedComponent, {
- info: {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
-});
-
-export const ActiveErrorSync = createExample(TestedComponent, {
- info: {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- lastAttemptedBackupTimestamp: {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
- },
- lastError: {
- code: 2002,
- details: 'details',
- hint: 'error hint from the server',
- message: 'message'
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
-});
-
-export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
- info: {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
- },
- backupProblem: {
- type: 'backup-unreadable'
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
-});
-
-export const ActiveBackupProblemDevice = createExample(TestedComponent, {
- info: {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
- },
- backupProblem: {
- type: 'backup-conflicting-device',
- myDeviceId: 'my-device-id',
- otherDeviceId: 'other-device-id',
- backupTimestamp: {
- "t_ms": 1656599921000
- }
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
-});
-
-export const InactiveUnpaid = createExample(TestedComponent, {
- info: {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.Unpaid,
- },
- "terms": {
- "annualFee": "EUR:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
-});
-
-export const InactiveInsufficientBalance = createExample(TestedComponent, {
- info: {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.InsufficientBalance,
- },
- "terms": {
- "annualFee": "EUR:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
-});
-
-export const InactivePending = createExample(TestedComponent, {
- info: {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.Pending,
- },
- "terms": {
- "annualFee": "EUR:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
-});
-
-
-export const ActiveTermsChanged = createExample(TestedComponent, {
- info: {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.TermsChanged,
- paidUntil: {
- t_ms: 1656599921000
- },
- newTerms: {
- "annualFee": "EUR:10",
- "storageLimitInMegabytes": 8,
- "supportedProtocolVersion": "0.0"
- },
- oldTerms: {
- "annualFee": "EUR:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- },
- "terms": {
- "annualFee": "EUR:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
-});
-
diff --git a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx
deleted file mode 100644
index 04adbb21c..000000000
--- a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-
-import { i18n, Timestamp } from "@gnu-taler/taler-util";
-import { ProviderInfo, ProviderPaymentStatus, ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
-import { format, formatDuration, intervalToDuration } from "date-fns";
-import { Fragment, VNode, h } from "preact";
-import { ErrorMessage } from "../components/ErrorMessage";
-import { Button, ButtonDestructive, ButtonPrimary, PaymentStatus, PopupBox, SmallLightText } from "../components/styled";
-import { useProviderStatus } from "../hooks/useProviderStatus";
-
-interface Props {
- pid: string;
- onBack: () => void;
-}
-
-export function ProviderDetailPage({ pid, onBack }: Props): VNode {
- const status = useProviderStatus(pid)
- if (!status) {
- return <div><i18n.Translate>Loading...</i18n.Translate></div>
- }
- if (!status.info) {
- onBack()
- return <div />
- }
- return <ProviderView info={status.info}
- onSync={status.sync}
- onDelete={() => status.remove().then(onBack)}
- onBack={onBack}
- onExtend={() => { null }}
- />;
-}
-
-export interface ViewProps {
- info: ProviderInfo;
- onDelete: () => void;
- onSync: () => void;
- onBack: () => void;
- onExtend: () => void;
-}
-
-export function ProviderView({ info, onDelete, onSync, onBack, onExtend }: ViewProps): VNode {
- const lb = info?.lastSuccessfulBackupTimestamp
- const isPaid = info.paymentStatus.type === ProviderPaymentType.Paid || info.paymentStatus.type === ProviderPaymentType.TermsChanged
- return (
- <PopupBox>
- <Error info={info} />
- <header>
- <h3>{info.name} <SmallLightText>{info.syncProviderBaseUrl}</SmallLightText></h3>
- <PaymentStatus color={isPaid ? 'rgb(28, 184, 65)' : 'rgb(202, 60, 60)'}>{isPaid ? 'Paid' : 'Unpaid'}</PaymentStatus>
- </header>
- <section>
- <p><b>Last backup:</b> {lb == null || lb.t_ms == "never" ? "never" : format(lb.t_ms, 'dd MMM yyyy')} </p>
- <ButtonPrimary onClick={onSync}><i18n.Translate>Back up</i18n.Translate></ButtonPrimary>
- {info.terms && <Fragment>
- <p><b>Provider fee:</b> {info.terms && info.terms.annualFee} per year</p>
- </Fragment>
- }
- <p>{descriptionByStatus(info.paymentStatus)}</p>
- <ButtonPrimary disabled onClick={onExtend}><i18n.Translate>Extend</i18n.Translate></ButtonPrimary>
-
- {info.paymentStatus.type === ProviderPaymentType.TermsChanged && <div>
- <p><i18n.Translate>terms has changed, extending the service will imply accepting the new terms of service</i18n.Translate></p>
- <table>
- <thead>
- <tr>
- <td></td>
- <td><i18n.Translate>old</i18n.Translate></td>
- <td> -&gt;</td>
- <td><i18n.Translate>new</i18n.Translate></td>
- </tr>
- </thead>
- <tbody>
-
- <tr>
- <td><i18n.Translate>fee</i18n.Translate></td>
- <td>{info.paymentStatus.oldTerms.annualFee}</td>
- <td>-&gt;</td>
- <td>{info.paymentStatus.newTerms.annualFee}</td>
- </tr>
- <tr>
- <td><i18n.Translate>storage</i18n.Translate></td>
- <td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
- <td>-&gt;</td>
- <td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
- </tr>
- </tbody>
- </table>
- </div>}
-
- </section>
- <footer>
- <Button onClick={onBack}><i18n.Translate> &lt; back</i18n.Translate></Button>
- <div>
- <ButtonDestructive onClick={onDelete}><i18n.Translate>remove provider</i18n.Translate></ButtonDestructive>
- </div>
- </footer>
- </PopupBox>
- )
-}
-
-function daysSince(d?: Timestamp) {
- if (!d || d.t_ms === 'never') return 'never synced'
- const duration = intervalToDuration({
- start: d.t_ms,
- end: new Date(),
- })
- const str = formatDuration(duration, {
- delimiter: ', ',
- format: [
- duration?.years ? i18n.str`years` : (
- duration?.months ? i18n.str`months` : (
- duration?.days ? i18n.str`days` : (
- duration?.hours ? i18n.str`hours` : (
- duration?.minutes ? i18n.str`minutes` : i18n.str`seconds`
- )
- )
- )
- )
- ]
- })
- return `synced ${str} ago`
-}
-
-function Error({ info }: { info: ProviderInfo }) {
- if (info.lastError) {
- return <ErrorMessage title={info.lastError.hint} />
- }
- if (info.backupProblem) {
- switch (info.backupProblem.type) {
- case "backup-conflicting-device":
- return <ErrorMessage title={<Fragment>
- <i18n.Translate>There is conflict with another backup from <b>{info.backupProblem.otherDeviceId}</b></i18n.Translate>
- </Fragment>} />
- case "backup-unreadable":
- return <ErrorMessage title="Backup is not readable" />
- default:
- return <ErrorMessage title={<Fragment>
- <i18n.Translate>Unknown backup problem: {JSON.stringify(info.backupProblem)}</i18n.Translate>
- </Fragment>} />
- }
- }
- return null
-}
-
-function colorByStatus(status: ProviderPaymentType) {
- switch (status) {
- case ProviderPaymentType.InsufficientBalance:
- return 'rgb(223, 117, 20)'
- case ProviderPaymentType.Unpaid:
- return 'rgb(202, 60, 60)'
- case ProviderPaymentType.Paid:
- return 'rgb(28, 184, 65)'
- case ProviderPaymentType.Pending:
- return 'gray'
- case ProviderPaymentType.InsufficientBalance:
- return 'rgb(202, 60, 60)'
- case ProviderPaymentType.TermsChanged:
- return 'rgb(202, 60, 60)'
- }
-}
-
-function descriptionByStatus(status: ProviderPaymentStatus) {
- switch (status.type) {
- // return i18n.str`no enough balance to make the payment`
- // return i18n.str`not paid yet`
- case ProviderPaymentType.Paid:
- case ProviderPaymentType.TermsChanged:
- if (status.paidUntil.t_ms === 'never') {
- return i18n.str`service paid`
- } else {
- return <Fragment>
- <b>Backup valid until:</b> {format(status.paidUntil.t_ms, 'dd MMM yyyy')}
- </Fragment>
- }
- case ProviderPaymentType.Unpaid:
- case ProviderPaymentType.InsufficientBalance:
- case ProviderPaymentType.Pending:
- return ''
- }
-}
diff --git a/packages/taler-wallet-webextension/src/popup/Settings.tsx b/packages/taler-wallet-webextension/src/popup/Settings.tsx
deleted file mode 100644
index 8595c87ff..000000000
--- a/packages/taler-wallet-webextension/src/popup/Settings.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-
-import { i18n } from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
-import { Checkbox } from "../components/Checkbox";
-import { EditableText } from "../components/EditableText";
-import { SelectList } from "../components/SelectList";
-import { PopupBox } from "../components/styled";
-import { useDevContext } from "../context/devContext";
-import { useBackupDeviceName } from "../hooks/useBackupDeviceName";
-import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
-import { useLang } from "../hooks/useLang";
-
-export function SettingsPage(): VNode {
- const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
- const { devMode, toggleDevMode } = useDevContext()
- const { name, update } = useBackupDeviceName()
- const [lang, changeLang] = useLang()
- return <SettingsView
- lang={lang} changeLang={changeLang}
- deviceName={name} setDeviceName={update}
- permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions}
- developerMode={devMode} toggleDeveloperMode={toggleDevMode}
- />;
-}
-
-export interface ViewProps {
- lang: string;
- changeLang: (s: string) => void;
- deviceName: string;
- setDeviceName: (s: string) => Promise<void>;
- permissionsEnabled: boolean;
- togglePermissions: () => void;
- developerMode: boolean;
- toggleDeveloperMode: () => void;
-}
-
-import { strings as messages } from '../i18n/strings'
-
-type LangsNames = {
- [P in keyof typeof messages]: string
-}
-
-const names: LangsNames = {
- es: 'Español [es]',
- en: 'English [en]',
- fr: 'Français [fr]',
- de: 'Deutsch [de]',
- sv: 'Svenska [sv]',
- it: 'Italiano [it]',
-}
-
-
-export function SettingsView({ lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode {
- return (
- <PopupBox>
- <section>
- {/* <h2><i18n.Translate>Wallet</i18n.Translate></h2> */}
- {/* <SelectList
- value={lang}
- onChange={changeLang}
- name="lang"
- list={names}
- label={i18n.str`Language`}
- description="(Choose your preferred lang)"
- />
- <EditableText
- value={deviceName}
- onChange={setDeviceName}
- name="device-id"
- label={i18n.str`Device name`}
- description="(This is how you will recognize the wallet in the backup provider)"
- /> */}
- <h2><i18n.Translate>Permissions</i18n.Translate></h2>
- <Checkbox label="Automatically open wallet based on page content"
- name="perm"
- description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)"
- enabled={permissionsEnabled} onToggle={togglePermissions}
- />
- <h2>Config</h2>
- <Checkbox label="Developer mode"
- name="devMode"
- description="(More options and information useful for debugging)"
- enabled={developerMode} onToggle={toggleDeveloperMode}
- />
- </section>
- <footer style={{ justifyContent: 'space-around' }}>
- <a target="_blank"
- rel="noopener noreferrer"
- style={{ color: 'darkgreen', textDecoration: 'none' }}
- href={chrome.extension ? chrome.extension.getURL(`/static/wallet.html#/settings`) : '#'}>VIEW MORE SETTINGS</a>
- </footer>
- </PopupBox>
- )
-} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
index 88c7c725e..0388664b3 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,38 +15,33 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { TalerActionFound as TestedComponent } from './TalerActionFound';
+import * as tests from "@gnu-taler/web-util/testing";
+import { TalerActionFound as TestedComponent } from "./TalerActionFound.js";
export default {
- title: 'popup/TalerActionFound',
- component: TestedComponent,
+ title: "TalerActionFound",
};
-export const PayAction = createExample(TestedComponent, {
- url: 'taler://pay/something'
-});
-
-export const WithdrawalAction = createExample(TestedComponent, {
- url: 'taler://withdraw/something'
+export const PayAction = tests.createExample(TestedComponent, {
+ url: "taler://pay/something",
});
-export const TipAction = createExample(TestedComponent, {
- url: 'taler://tip/something'
+export const WithdrawalAction = tests.createExample(TestedComponent, {
+ url: "taler://withdraw/something",
});
-export const NotifyAction = createExample(TestedComponent, {
- url: 'taler://notify-reserve/something'
+export const NotifyAction = tests.createExample(TestedComponent, {
+ url: "taler://notify-reserve/something",
});
-export const RefundAction = createExample(TestedComponent, {
- url: 'taler://refund/something'
+export const RefundAction = tests.createExample(TestedComponent, {
+ url: "taler://refund/something",
});
-export const InvalidAction = createExample(TestedComponent, {
- url: 'taler://something/asd'
+export const InvalidAction = tests.createExample(TestedComponent, {
+ url: "taler://something/asd",
});
diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
index ef0ec341c..21373c7cd 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
@@ -1,98 +1,136 @@
-import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
-import { ButtonPrimary, ButtonSuccess, PopupBox } from "../components/styled/index";
+/*
+ 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 { parseTalerUri, TalerUri, TalerUriAction } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { Title } from "../components/styled/index.js";
+import { Button } from "../mui/Button.js";
+import { platform } from "../platform/foreground.js";
export interface Props {
url: string;
- onDismiss: () => void;
+ onDismiss: () => Promise<void>;
}
-export function TalerActionFound({ url, onDismiss }: Props) {
- const uriType = classifyTalerUri(url);
- return <PopupBox>
- <section>
- <h1>Taler Action </h1>
- {uriType === TalerUriType.TalerPay && <div>
- <p>This page has pay action.</p>
- <ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}>
- Open pay page
- </ButtonSuccess>
- </div>}
- {uriType === TalerUriType.TalerWithdraw && <div>
- <p>This page has a withdrawal action.</p>
- <ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}>
- Open withdraw page
- </ButtonSuccess>
- </div>}
- {uriType === TalerUriType.TalerTip && <div>
- <p>This page has a tip action.</p>
- <ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}>
- Open tip page
- </ButtonSuccess>
- </div>}
- {uriType === TalerUriType.TalerNotifyReserve && <div>
- <p>This page has a notify reserve action.</p>
- <ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}>
- Notify
- </ButtonSuccess>
- </div>}
- {uriType === TalerUriType.TalerRefund && <div>
- <p>This page has a refund action.</p>
- <ButtonSuccess onClick={() => { chrome.tabs.create({ "url": actionForTalerUri(uriType, url) }); }}>
- Open refund page
- </ButtonSuccess>
- </div>}
- {uriType === TalerUriType.Unknown && <div>
- <p>This page has a malformed taler uri.</p>
- <p>{url}</p>
- </div>}
-
- </section>
- <footer>
- <div />
- <ButtonPrimary onClick={() => onDismiss()}> Dismiss </ButtonPrimary>
- </footer>
- </PopupBox>;
+function ContentByUriType({
+ uri,
+ onConfirm,
+}: {
+ uri: TalerUri;
+ onConfirm: () => Promise<void>;
+}) {
+ const { i18n } = useTranslationContext();
+ switch (uri.type) {
+ case TalerUriAction.WithdrawExchange:
+ case TalerUriAction.Withdraw:
+ return (
+ <div>
+ <p>
+ <i18n.Translate>This page has a withdrawal action.</i18n.Translate>
+ </p>
+ <Button variant="contained" color="success" onClick={onConfirm}>
+ <i18n.Translate>Open withdraw page</i18n.Translate>
+ </Button>
+ </div>
+ );
-}
+ case TalerUriAction.PayTemplate:
+ case TalerUriAction.Pay:
+ return (
+ <div>
+ <p>
+ <i18n.Translate>This page has pay action.</i18n.Translate>
+ </p>
+ <Button variant="contained" color="success" onClick={onConfirm}>
+ <i18n.Translate>Open pay page</i18n.Translate>
+ </Button>
+ </div>
+ );
-function actionForTalerUri(uriType: TalerUriType, talerUri: string): string | undefined {
- switch (uriType) {
- case TalerUriType.TalerWithdraw:
- return makeExtensionUrlWithParams("static/wallet.html#/withdraw", {
- talerWithdrawUri: talerUri,
- });
- case TalerUriType.TalerPay:
- return makeExtensionUrlWithParams("static/wallet.html#/pay", {
- talerPayUri: talerUri,
- });
- case TalerUriType.TalerTip:
- return makeExtensionUrlWithParams("static/wallet.html#/tip", {
- talerTipUri: talerUri,
- });
- case TalerUriType.TalerRefund:
- return makeExtensionUrlWithParams("static/wallet.html#/refund", {
- talerRefundUri: talerUri,
- });
- case TalerUriType.TalerNotifyReserve:
- // FIXME: implement
- break;
- default:
- console.warn(
- "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
+ case TalerUriAction.Refund:
+ return (
+ <div>
+ <p>
+ <i18n.Translate>This page has a refund action.</i18n.Translate>
+ </p>
+ <Button variant="contained" color="success" onClick={onConfirm}>
+ <i18n.Translate>Open refund page</i18n.Translate>
+ </Button>
+ </div>
);
- break;
+ case TalerUriAction.AddExchange:
+ return (
+ <div>
+ <p>
+ <i18n.Translate>This page has a add exchange action.</i18n.Translate>
+ </p>
+ <Button variant="contained" color="success" onClick={onConfirm}>
+ <i18n.Translate>Open add exchange page</i18n.Translate>
+ </Button>
+ </div>
+ );
+
+ case TalerUriAction.DevExperiment:
+ case TalerUriAction.PayPull:
+ case TalerUriAction.PayPush:
+ case TalerUriAction.Restore:
+ return null;
+ default: {
+ const error: never = uri;
+ return null;
+ }
}
- return undefined;
}
-function makeExtensionUrlWithParams(
- url: string,
- params?: { [name: string]: string | undefined },
-): string {
- const innerUrl = new URL(chrome.extension.getURL("/" + url));
- if (params) {
- const hParams = Object.keys(params).map(k => `${k}=${params[k]}`).join('&')
- innerUrl.hash = innerUrl.hash + '?' + hParams
+export function TalerActionFound({ url, onDismiss }: Props): VNode {
+ const talerUri = parseTalerUri(url);
+ const { i18n } = useTranslationContext();
+ async function redirectToWallet(): Promise<void> {
+ platform.openWalletURIFromPopup(talerUri!);
}
- return innerUrl.href;
+ return (
+ <Fragment>
+ <section>
+ <Title>
+ <i18n.Translate>Taler Action</i18n.Translate>
+ </Title>
+ {!talerUri ? (
+ <div>
+ <p>
+ <i18n.Translate>
+ This page has a malformed taler uri.
+ </i18n.Translate>
+ </p>
+ </div>
+ ) : (
+ <ContentByUriType uri={talerUri} onConfirm={redirectToWallet} />
+ )}
+ </section>
+ <footer>
+ <div />
+ <Button variant="contained" onClick={onDismiss}>
+ <i18n.Translate>Dismiss</i18n.Translate>
+ </Button>
+ </footer>
+ </Fragment>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/popup/index.stories.tsx b/packages/taler-wallet-webextension/src/popup/index.stories.tsx
new file mode 100644
index 000000000..ea7cee77d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/popup/index.stories.tsx
@@ -0,0 +1,23 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export * as a1 from "./Balance.stories.js";
+export * as a2 from "./TalerActionFound.stories.js";
diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.dev.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.dev.tsx
new file mode 100644
index 000000000..f0bc81399
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/popupEntryPoint.dev.tsx
@@ -0,0 +1,53 @@
+/*
+ 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/>
+ */
+
+/**
+ * Main entry point for extension pages.
+ *
+ * @author sebasjm
+ */
+
+import { setupI18n } from "@gnu-taler/taler-util";
+import { h, render } from "preact";
+import { strings } from "./i18n/strings.js";
+import { setupPlatform } from "./platform/foreground.js";
+import devAPI from "./platform/dev.js";
+import { Application } from "./popup/Application.js";
+
+setupPlatform(devAPI);
+
+function main(): void {
+ try {
+ const container = document.getElementById("container");
+ if (!container) {
+ throw Error("container not found, can't mount page contents");
+ }
+ render(<Application />, container);
+ } catch (e) {
+ console.error("got error", e);
+ if (e instanceof Error) {
+ document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
+ }
+ }
+}
+
+setupI18n("en", strings);
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
index 070df554c..08915ea96 100644
--- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -17,28 +17,25 @@
/**
* Main entry point for extension pages.
*
- * @author Florian Dold <dold@taler.net>
+ * @author sebasjm
*/
import { setupI18n } from "@gnu-taler/taler-util";
-import { createHashHistory } from "history";
-import { render, h, VNode } from "preact";
-import Router, { route, Route, getCurrentUrl } from "preact-router";
-import { useEffect, useState } from "preact/hooks";
-import { DevContextProvider } from "./context/devContext";
-import { useTalerActionURL } from "./hooks/useTalerActionURL";
-import { strings } from "./i18n/strings";
-import { BackupPage } from "./popup/BackupPage";
-import { BalancePage } from "./popup/BalancePage";
-import { DeveloperPage as DeveloperPage } from "./popup/Debug";
-import { HistoryPage } from "./popup/History";
-import {
- Pages, WalletNavBar
-} from "./NavigationBar";
-import { ProviderAddPage } from "./popup/ProviderAddPage";
-import { ProviderDetailPage } from "./popup/ProviderDetailPage";
-import { SettingsPage } from "./popup/Settings";
-import { TalerActionFound } from "./popup/TalerActionFound";
+import { h, render } from "preact";
+import { strings } from "./i18n/strings.js";
+import { setupPlatform } from "./platform/foreground.js";
+import chromeAPI from "./platform/chrome.js";
+import firefoxAPI from "./platform/firefox.js";
+import { Application } from "./popup/Application.js";
+
+//FIXME: create different entry point for any platform instead of
+//switching in runtime
+const isFirefox = typeof (window as any)["InstallTrigger"] !== "undefined";
+if (isFirefox) {
+ setupPlatform(firefoxAPI);
+} else {
+ setupPlatform(chromeAPI);
+}
function main(): void {
try {
@@ -55,77 +52,10 @@ function main(): void {
}
}
-setupI18n("en-US", strings);
+setupI18n("en", strings);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", main);
} else {
main();
}
-
-function Application() {
- const [talerActionUrl, setDismissed] = useTalerActionURL()
-
- useEffect(() => {
- if (talerActionUrl) route(Pages.cta)
- },[talerActionUrl])
-
- return (
- <div>
- <DevContextProvider>
- <WalletNavBar />
- <div style={{ width: 400, height: 290 }}>
- <Router history={createHashHistory()}>
- <Route path={Pages.dev} component={DeveloperPage} />
-
- <Route path={Pages.balance} component={BalancePage}
- goToWalletManualWithdraw={() => goToWalletPage(Pages.manual_withdraw)}
- />
- <Route path={Pages.settings} component={SettingsPage} />
- <Route path={Pages.cta} component={() => <TalerActionFound url={talerActionUrl!} onDismiss={() => {
- setDismissed(true)
- route(Pages.balance)
- }} />} />
-
- <Route path={Pages.transaction}
- component={({ tid }: { tid: string }) => goToWalletPage(Pages.transaction.replace(':tid', tid))}
- />
-
- <Route path={Pages.history} component={HistoryPage} />
- <Route path={Pages.backup} component={BackupPage}
- onAddProvider={() => {
- route(Pages.provider_add)
- }}
- />
- <Route path={Pages.provider_detail} component={ProviderDetailPage}
- onBack={() => {
- route(Pages.backup)
- }}
- />
- <Route path={Pages.provider_add} component={ProviderAddPage}
- onBack={() => {
- route(Pages.backup)
- }}
- />
- <Route default component={Redirect} to={Pages.balance} />
- </Router>
- </div>
- </DevContextProvider>
- </div>
- );
-}
-
-function goToWalletPage(page: Pages | string): null {
- chrome.tabs.create({
- active: true,
- url: chrome.extension.getURL(`/static/wallet.html#${page}`),
- })
- return null
-}
-
-function Redirect({ to }: { to: string }): null {
- useEffect(() => {
- route(to, true)
- })
- return null
-}
diff --git a/packages/taler-wallet-webextension/src/pwa/index.html b/packages/taler-wallet-webextension/src/pwa/index.html
new file mode 100644
index 000000000..c150ee68d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/index.html
@@ -0,0 +1,114 @@
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <link rel="manifest" href="./manifest.json" />
+ <style>
+ .overlay {
+ position: absolute;
+ top: 0px;
+ display: none;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ color: white;
+ justify-content: center;
+ }
+ .overlay > iframe {
+ margin: auto;
+ }
+ </style>
+ </head>
+ <body>
+ <script>
+ function openPopup() {
+ document.getElementById("popup-overlay").style.display = "flex";
+ window.frames["popup"].location = "popup.html";
+ }
+ function closePopup() {
+ document.getElementById("popup-overlay").style.display = "none";
+ }
+ function redirectWallet(url) {
+ window.frames["wallet"].location = url;
+ }
+ function openWallet() {
+ redirectWallet("wallet.html");
+ }
+ function closeWallet() {
+ redirectWallet("about:blank");
+ }
+ function reloadWallet() {
+ window.frames["wallet"].location.reload()
+ }
+ function openPage() {
+ window.frames["other"].location =
+ document.getElementById("page-url").value;
+ }
+ </script>
+ <button value="asd" onclick="openPopup()">open popup</button>
+ <button value="asd" onclick="closeWallet();openWallet()">
+ restart
+ </button>
+ <button value="asd" onclick="reloadWallet()">
+ refresh
+ </button>
+ <br />
+ <iframe
+ id="wallet-window"
+ name="wallet"
+ src="wallet.html"
+ style="height: calc(100% - 30px)"
+ width="850"
+ height="90%"
+ >
+ </iframe>
+ <!-- <input id="page-url" type="text" />
+ <button onclick="openPage()">open</button> -->
+ <!-- <a
+ href='javascript:void(window.frames["other"].location = "http://bank.taler:5882")'
+ >open local bank</a
+ >
+ <hr />
+ <iframe
+ id="other-window"
+ name="other"
+ src="http://bank.taler:5882"
+ width="100%"
+ height="325"
+ >
+ </iframe> -->
+ <div class="overlay" id="popup-overlay" onclick="closePopup()">
+
+ <iframe
+ id="popup-window"
+ name="popup"
+ src="about:blank"
+ width="500"
+ height="325"
+ >
+ </iframe>
+ </div>
+ <!-- <hr />
+ <iframe src="tests.html" name="wallet" width="800" height="100%"> </iframe> -->
+ <!-- <hr />
+ <iframe src="stories.html" name="wallet" width="800" height="100%"> -->
+ <script type="module" src="background.dev.js"></script>
+ <script type="module">
+ if ("serviceWorker" in navigator) {
+ try {
+ const registration = await navigator.serviceWorker.register("sw.js", {
+ scope: "/app/",
+ });
+ if (registration.installing) {
+ console.log("Service worker installing");
+ } else if (registration.waiting) {
+ console.log("Service worker installed");
+ } else if (registration.active) {
+ console.log("Service worker active");
+ }
+ } catch (error) {
+ console.error(`Registration failed with ${error}`);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/src/pwa/manifest.json b/packages/taler-wallet-webextension/src/pwa/manifest.json
new file mode 100644
index 000000000..adf27e43f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "GNU Taler Wallet",
+ "description": "Privacy preserving and transparent payments",
+ "author": "GNU Taler Developers",
+ "version": "0.9.3.13",
+ "id": "gnu-taler-wallet-web-spa-development",
+ "version_name": "0.9.3-dev.13",
+ "display": "minimal-ui",
+ "start_url": "./",
+ "manifest_version": 3,
+ "minimum_chrome_version": "88",
+ "icons": [
+ {
+ "src": "./static/img/taler-logo-48.png",
+ "type": "image/png",
+ "sizes": "48x48"
+ },
+ {
+ "src": "./static/img/taler-logo-128.png",
+ "type": "image/png",
+ "sizes": "128x128"
+ },
+ {
+ "src": "./static/img/taler-logo-512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "protocol_handlers": [
+ {
+ "protocol": "web+taler",
+ "url": "./wallet.html?type=%s"
+ }
+ ]
+}
diff --git a/packages/taler-wallet-webextension/src/pwa/popup.html b/packages/taler-wallet-webextension/src/pwa/popup.html
new file mode 100644
index 000000000..34d1d019c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/popup.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <style>
+ html {
+ font-family: sans-serif; /* 1 */
+ }
+ body {
+ margin: 0;
+ }
+ </style>
+ <style>
+ html {
+ }
+ h1 {
+ font-size: 2em;
+ }
+ input {
+ font: inherit;
+ }
+ body {
+ margin: 0;
+ font-size: 100%;
+ padding: 0;
+ overflow: hidden;
+ background-color: #f8faf7;
+ font-family: Arial, Helvetica, sans-serif;
+ }
+ </style>
+
+ <link rel="stylesheet" type="text/css" href="popupEntryPoint.dev.css" />
+ <script type="module" src="popupEntryPoint.dev.js"></script>
+ </head>
+
+ <body>
+ <taler-popup id="container" class="popup-container"></taler-popup>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/src/pwa/static/font/import.css b/packages/taler-wallet-webextension/src/pwa/static/font/import.css
new file mode 100644
index 000000000..d726ebc5a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/import.css
@@ -0,0 +1,35 @@
+@font-face {
+ font-family: "Roboto";
+ font-style: italic;
+ font-weight: 400;
+ font-display: swap;
+ src: url(/static/font/roboto-italic-400.ttf) format("truetype");
+}
+@font-face {
+ font-family: "Roboto";
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(/static/font/roboto-normal-300.ttf) format("truetype");
+}
+@font-face {
+ font-family: "Roboto";
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(/static/font/roboto-normal-400.ttf) format("truetype");
+}
+@font-face {
+ font-family: "Roboto";
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url(/static/font/roboto-normal-500.ttf) format("truetype");
+}
+@font-face {
+ font-family: "Roboto";
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(/static/font/roboto-normal-700.ttf) format("truetype");
+}
diff --git a/packages/taler-wallet-webextension/src/pwa/static/font/roboto-italic-400.ttf b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-italic-400.ttf
new file mode 100644
index 000000000..1e746d17f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-italic-400.ttf
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-300.tff b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-300.tff
new file mode 100644
index 000000000..ec821b577
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-300.tff
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-400.ttf b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-400.ttf
new file mode 100644
index 000000000..9d4b32b47
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-400.ttf
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-500.ttf b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-500.ttf
new file mode 100644
index 000000000..4b4e1c656
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-500.ttf
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-700.ttf b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-700.ttf
new file mode 100644
index 000000000..58d877c58
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/font/roboto-normal-700.ttf
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-128.png b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-128.png
new file mode 100644
index 000000000..a2f0c22eb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-128.png
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-2022.svg b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-2022.svg
new file mode 100644
index 000000000..2ac2785b8
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-2022.svg
@@ -0,0 +1,468 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="670"
+ height="300"
+ viewBox="0 0 201 90"
+ version="1.1"
+ id="svg8"
+ sodipodi:docname="taler-logo-2023.svg"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)">
+ <metadata
+ id="metadata67">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs854">
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20663">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20665"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20667">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20669"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20671">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20673"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20675">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20677"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20679">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20681"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20683">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20685"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20687">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20689"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20691">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20693"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20695">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20697"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20699">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20701"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20703">
+ <rect
+ style="fill:#00ff00;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20705"
+ width="164.73636"
+ height="53.465477"
+ x="12.38413"
+ y="263.48923" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20707">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20709"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20711">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20713"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20715">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20717"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20719">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20721"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20723">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20725"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20727">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20729"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20731">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20733"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20735">
+ <rect
+ style="opacity:1;fill:#00ff00;fill-opacity:1;stroke:#df373a;stroke-width:0.103816;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect20737"
+ width="164.73636"
+ height="53.465477"
+ x="-16.523348"
+ y="98.188889" />
+ </clipPath>
+ </defs>
+ <sodipodi:namedview
+ id="namedview852"
+ pagecolor="#000000"
+ bordercolor="#cccccc"
+ borderopacity="1"
+ inkscape:pageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="false"
+ showgrid="false"
+ inkscape:zoom="0.46315494"
+ inkscape:cx="-659.30808"
+ inkscape:cy="83.54417"
+ inkscape:window-width="1920"
+ inkscape:window-height="1025"
+ inkscape:window-x="0"
+ inkscape:window-y="26"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="logo" />
+ <g
+ id="logo">
+ <g
+ id="circles"
+ style="display:inline;fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943"
+ transform="translate(180)">
+ <g
+ id="g4645"
+ inkscape:export-xdpi="98.304001"
+ inkscape:export-ydpi="98.304001">
+ <ellipse
+ transform="matrix(-0.99007841,-0.140516,0.16039263,-0.98705329,0,0)"
+ ry="75.234604"
+ rx="74.764656"
+ cy="-29.611343"
+ cx="101.25517"
+ id="path4580"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.1471165;stroke-opacity:1" />
+ <g
+ transform="rotate(-180,-107.57659,26.234233)"
+ id="g4622">
+ <path
+ id="path1306-7-63-9"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="M 45.48017,110.87571 A 35.545008,38.588202 0 0 0 9.9354536,149.46424 35.545008,38.588202 0 0 0 45.48017,188.05226 35.545008,38.588202 0 0 0 81.025385,149.46424 35.545008,38.588202 0 0 0 45.48017,110.87571 Z m -0.07061,4.90892 a 31.151221,33.78691 0 0 1 0.829048,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.827519,0.0595 31.151221,33.78691 0 0 1 0.825964,0.0839 31.151221,33.78691 0 0 1 0.823425,0.10767 31.151221,33.78691 0 0 1 0.820349,0.13097 31.151221,33.78691 0 0 1 0.816775,0.15478 31.151221,33.78691 0 0 1 0.813198,0.17859 31.151221,33.78691 0 0 1 0.808083,0.20188 31.151221,33.78691 0 0 1 0.802953,0.22518 31.151221,33.78691 0 0 1 0.797339,0.24796 31.151221,33.78691 0 0 1 0.790697,0.27125 31.151221,33.78691 0 0 1 0.783536,0.29403 31.151221,33.78691 0 0 1 0.776355,0.31629 31.151221,33.78691 0 0 1 0.768177,0.33854 31.151221,33.78691 0 0 1 0.760022,0.3608 31.151221,33.78691 0 0 1 0.750783,0.38255 31.151221,33.78691 0 0 1 0.740582,0.40429 31.151221,33.78691 0 0 1 0.73085,0.42499 31.151221,33.78691 0 0 1 0.7201,0.44622 31.151221,33.78691 0 0 1 0.70885,0.46692 31.151221,33.78691 0 0 1 0.6971,0.48764 31.151221,33.78691 0 0 1 0.68484,0.50678 31.151221,33.78691 0 0 1 0.67254,0.52697 31.151221,33.78691 0 0 1 0.65872,0.54613 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.58339 31.151221,33.78691 0 0 1 0.61678,0.601 31.151221,33.78691 0 0 1 0.60196,0.61911 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65277 31.151221,33.78691 0 0 1 0.5544,0.66932 31.151221,33.78691 0 0 1 0.53751,0.68486 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71488 31.151221,33.78691 0 0 1 0.48534,0.72885 31.151221,33.78691 0 0 1 0.46798,0.74284 31.151221,33.78691 0 0 1 0.44904,0.75629 31.151221,33.78691 0 0 1 0.43065,0.76872 31.151221,33.78691 0 0 1 0.41119,0.78114 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80391 31.151221,33.78691 0 0 1 0.35239,0.81376 31.151221,33.78691 0 0 1 0.33294,0.8241 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.84171 31.151221,33.78691 0 0 1 0.27107,0.85051 31.151221,33.78691 0 0 1 0.2501,0.85775 31.151221,33.78691 0 0 1 0.2286,0.86448 31.151221,33.78691 0 0 1 0.20765,0.8707 31.151221,33.78691 0 0 1 0.18616,0.8769 31.151221,33.78691 0 0 1 0.16468,0.88157 31.151221,33.78691 0 0 1 0.14268,0.8857 31.151221,33.78691 0 0 1 0.12073,0.88985 31.151221,33.78691 0 0 1 0.0992,0.89347 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.6509 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60173 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40916 31.151221,33.78691 0 0 1 -1.19526,2.31444 31.151221,33.78691 0 0 1 -1.3589,2.20624 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64252 31.151221,33.78691 0 0 1 -2.034,1.47428 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.221695,1.11037 31.151221,33.78691 0 0 1 -2.294328,0.91832 31.151221,33.78691 0 0 1 -2.354157,0.72006 31.151221,33.78691 0 0 1 -2.399184,0.51765 31.151221,33.78691 0 0 1 -2.428831,0.31163 31.151221,33.78691 0 0 1 -2.444189,0.10457 31.151221,33.78691 0 0 1 -2.44418,-0.10457 31.151221,33.78691 0 0 1 -2.428838,-0.31163 31.151221,33.78691 0 0 1 -2.399177,-0.51765 31.151221,33.78691 0 0 1 -2.354163,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91831 31.151221,33.78691 0 0 1 -2.221196,-1.11037 31.151221,33.78691 0 0 1 -2.134249,-1.29621 31.151221,33.78691 0 0 1 -2.033489,-1.47428 31.151221,33.78691 0 0 1 -1.920973,-1.64252 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20624 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.65091 31.151221,33.78691 0 0 1 0.09615,-2.65091 31.151221,33.78691 0 0 1 0.287434,-2.63433 31.151221,33.78691 0 0 1 0.477175,-2.60174 31.151221,33.78691 0 0 1 0.663854,-2.55358 31.151221,33.78691 0 0 1 0.846946,-2.48889 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31495 31.151221,33.78691 0 0 1 1.358905,-2.20572 31.151221,33.78691 0 0 1 1.513873,-2.08356 31.151221,33.78691 0 0 1 1.660643,-1.94793 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920973,-1.64252 31.151221,33.78691 0 0 1 2.033505,-1.47376 31.151221,33.78691 0 0 1 2.13425,-1.29621 31.151221,33.78691 0 0 1 2.221195,-1.11088 31.151221,33.78691 0 0 1 2.294841,-0.9178 31.151221,33.78691 0 0 1 2.354165,-0.72058 31.151221,33.78691 0 0 1 2.399176,-0.51713 31.151221,33.78691 0 0 1 2.428838,-0.31215 31.151221,33.78691 0 0 1 2.44418,-0.10405 z"
+ clip-path="url(#clipPath20735)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-0"
+ style="opacity:1;fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0376767;stroke-linejoin:round"
+ d="m 68.010803,105.31927 a 40.722405,43.678338 0 0 0 -40.72207,43.67871 40.722405,43.678338 0 0 0 40.72207,43.67812 40.722405,43.678338 0 0 0 40.722647,-43.67812 40.722405,43.678338 0 0 0 -40.722647,-43.67871 z m -0.0809,5.55644 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.94628,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12188 35.68863,38.243712 0 0 1 0.93984,0.14824 35.68863,38.243712 0 0 1 0.93574,0.1752 35.68863,38.243712 0 0 1 0.93165,0.20214 35.68863,38.243712 0 0 1 0.92578,0.22852 35.68863,38.243712 0 0 1 0.91992,0.25488 35.68863,38.243712 0 0 1 0.91348,0.28067 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33281 35.68863,38.243712 0 0 1 0.88945,0.35801 35.68863,38.243712 0 0 1 0.880067,0.3832 35.68863,38.243712 0 0 1 0.87071,0.4084 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52851 35.68863,38.243712 0 0 1 0.79863,0.55196 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59648 35.68863,38.243712 0 0 1 0.75469,0.61817 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68027 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71953 35.68863,38.243712 0 0 1 0.65391,0.73887 35.68863,38.243712 0 0 1 0.63515,0.75762 35.68863,38.243712 0 0 1 0.61582,0.77519 35.68863,38.243712 0 0 1 0.59649,0.79219 35.68863,38.243712 0 0 1 0.57715,0.80918 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84082 35.68863,38.243712 0 0 1 0.51446,0.85606 35.68863,38.243712 0 0 1 0.493358,0.87011 35.68863,38.243712 0 0 1 0.47109,0.88418 35.68863,38.243712 0 0 1 0.44941,0.89707 35.68863,38.243712 0 0 1 0.427152,0.90996 35.68863,38.243712 0 0 1 0.40372,0.9211 35.68863,38.243712 0 0 1 0.38145,0.93281 35.68863,38.243712 0 0 1 0.35742,0.94336 35.68863,38.243712 0 0 1 0.33399,0.95273 35.68863,38.243712 0 0 1 0.31054,0.9627 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97851 35.68863,38.243712 0 0 1 0.23789,0.98555 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99785 35.68863,38.243712 0 0 1 0.16348,1.00254 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01132 35.68863,38.243712 0 0 1 0.0885,1.01368 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01777 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98243 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89043 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.173062,2.72696 35.68863,38.243712 0 0 1 -1.369338,2.61973 35.68863,38.243712 0 0 1 -1.55683,2.49726 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46719 35.68863,38.243712 0 0 1 -2.54531,1.25684 35.68863,38.243712 0 0 1 -2.628507,1.03945 35.68863,38.243712 0 0 1 -2.69706,0.81504 35.68863,38.243712 0 0 1 -2.74864,0.58593 35.68863,38.243712 0 0 1 -2.78261,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58593 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03945 35.68863,38.243712 0 0 1 -2.54473,-1.25684 35.68863,38.243712 0 0 1 -2.44512,-1.46719 35.68863,38.243712 0 0 1 -2.32968,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49726 35.68863,38.243712 0 0 1 -1.36933,-2.61973 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89043 35.68863,38.243712 0 0 1 -0.54668,-2.94492 35.68863,38.243712 0 0 1 -0.3293,-2.98243 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00058 35.68863,38.243712 0 0 1 0.3293,-2.98184 35.68863,38.243712 0 0 1 0.54668,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81719 35.68863,38.243712 0 0 1 1.17305,-2.72695 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20489 35.68863,38.243712 0 0 1 2.05782,-2.03847 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.32968,-1.66817 35.68863,38.243712 0 0 1 2.44512,-1.46718 35.68863,38.243712 0 0 1 2.54473,-1.25743 35.68863,38.243712 0 0 1 2.6291,-1.03886 35.68863,38.243712 0 0 1 2.69707,-0.81563 35.68863,38.243712 0 0 1 2.74863,-0.58535 35.68863,38.243712 0 0 1 2.78262,-0.35332 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20731)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-5"
+ style="opacity:1;fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0376767;stroke-linejoin:round"
+ d="M 45.56102,105.31927 A 40.722405,43.678338 0 0 0 4.8389507,148.99798 40.722405,43.678338 0 0 0 45.56102,192.6761 40.722405,43.678338 0 0 0 86.283657,148.99798 40.722405,43.678338 0 0 0 45.56102,105.31927 Z m -0.0809,5.55644 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.94628,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12188 35.68863,38.243712 0 0 1 0.93984,0.14824 35.68863,38.243712 0 0 1 0.93574,0.1752 35.68863,38.243712 0 0 1 0.93165,0.20214 35.68863,38.243712 0 0 1 0.92578,0.22852 35.68863,38.243712 0 0 1 0.91992,0.25488 35.68863,38.243712 0 0 1 0.91348,0.28067 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33281 35.68863,38.243712 0 0 1 0.88945,0.35801 35.68863,38.243712 0 0 1 0.880067,0.3832 35.68863,38.243712 0 0 1 0.87071,0.4084 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52851 35.68863,38.243712 0 0 1 0.79863,0.55196 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59648 35.68863,38.243712 0 0 1 0.75469,0.61817 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68027 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71953 35.68863,38.243712 0 0 1 0.65391,0.73887 35.68863,38.243712 0 0 1 0.63515,0.75762 35.68863,38.243712 0 0 1 0.61582,0.77519 35.68863,38.243712 0 0 1 0.59649,0.79219 35.68863,38.243712 0 0 1 0.57715,0.80918 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84082 35.68863,38.243712 0 0 1 0.51446,0.85606 35.68863,38.243712 0 0 1 0.49336,0.87011 35.68863,38.243712 0 0 1 0.47109,0.88418 35.68863,38.243712 0 0 1 0.44941,0.89707 35.68863,38.243712 0 0 1 0.42715,0.90996 35.68863,38.243712 0 0 1 0.40371,0.9211 35.68863,38.243712 0 0 1 0.38145,0.93281 35.68863,38.243712 0 0 1 0.35742,0.94336 35.68863,38.243712 0 0 1 0.33399,0.95273 35.68863,38.243712 0 0 1 0.31054,0.9627 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97851 35.68863,38.243712 0 0 1 0.23789,0.98555 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99785 35.68863,38.243712 0 0 1 0.16348,1.00254 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01132 35.68863,38.243712 0 0 1 0.0885,1.01368 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01778 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98243 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89043 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.17305,2.72696 35.68863,38.243712 0 0 1 -1.36934,2.61972 35.68863,38.243712 0 0 1 -1.55683,2.49727 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46719 35.68863,38.243712 0 0 1 -2.54531,1.25683 35.68863,38.243712 0 0 1 -2.628507,1.03946 35.68863,38.243712 0 0 1 -2.69706,0.81504 35.68863,38.243712 0 0 1 -2.74864,0.58593 35.68863,38.243712 0 0 1 -2.78261,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58593 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03946 35.68863,38.243712 0 0 1 -2.54473,-1.25683 35.68863,38.243712 0 0 1 -2.44512,-1.46719 35.68863,38.243712 0 0 1 -2.32968,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49727 35.68863,38.243712 0 0 1 -1.36933,-2.61972 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89043 35.68863,38.243712 0 0 1 -0.546679,-2.94492 35.68863,38.243712 0 0 1 -0.3293004,-2.98243 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00059 35.68863,38.243712 0 0 1 0.3293004,-2.98184 35.68863,38.243712 0 0 1 0.546679,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81719 35.68863,38.243712 0 0 1 1.17305,-2.72695 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20489 35.68863,38.243712 0 0 1 2.05782,-2.03847 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.32968,-1.66817 35.68863,38.243712 0 0 1 2.44512,-1.46718 35.68863,38.243712 0 0 1 2.54473,-1.25743 35.68863,38.243712 0 0 1 2.6291,-1.03886 35.68863,38.243712 0 0 1 2.69707,-0.81563 35.68863,38.243712 0 0 1 2.74863,-0.58535 35.68863,38.243712 0 0 1 2.78262,-0.35332 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20727)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-6"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0427732;stroke-linejoin:round;stroke-opacity:1"
+ d="M 68.102923,99.029256 A 46.363577,49.444797 0 0 0 21.739728,148.47447 46.363577,49.444797 0 0 0 68.102923,197.91903 46.363577,49.444797 0 0 0 114.46677,148.47447 46.363577,49.444797 0 0 0 68.102923,99.029256 Z m -0.09212,6.290014 a 40.632485,43.292687 0 0 1 1.081384,0.0153 40.632485,43.292687 0 0 1 1.080714,0.0464 40.632485,43.292687 0 0 1 1.079381,0.0763 40.632485,43.292687 0 0 1 1.077365,0.10745 40.632485,43.292687 0 0 1 1.074041,0.13797 40.632485,43.292687 0 0 1 1.070034,0.16781 40.632485,43.292687 0 0 1 1.065366,0.19832 40.632485,43.292687 0 0 1 1.060709,0.22884 40.632485,43.292687 0 0 1 1.054026,0.25869 40.632485,43.292687 0 0 1 1.047355,0.28853 40.632485,43.292687 0 0 1 1.040022,0.31772 40.632485,43.292687 0 0 1 1.031346,0.34756 40.632485,43.292687 0 0 1 1.022009,0.37675 40.632485,43.292687 0 0 1 1.01266,0.40528 40.632485,43.292687 0 0 1 1.00198,0.43379 40.632485,43.292687 0 0 1 0.99134,0.46232 40.632485,43.292687 0 0 1 0.97929,0.49017 40.632485,43.292687 0 0 1 0.96598,0.51803 40.632485,43.292687 0 0 1 0.9533,0.54457 40.632485,43.292687 0 0 1 0.93927,0.57175 40.632485,43.292687 0 0 1 0.92461,0.5983 40.632485,43.292687 0 0 1 0.90926,0.62482 40.632485,43.292687 0 0 1 0.89328,0.64936 40.632485,43.292687 0 0 1 0.87724,0.67524 40.632485,43.292687 0 0 1 0.85922,0.69977 40.632485,43.292687 0 0 1 0.8419,0.72365 40.632485,43.292687 0 0 1 0.82388,0.74754 40.632485,43.292687 0 0 1 0.80452,0.77008 40.632485,43.292687 0 0 1 0.78518,0.7933 40.632485,43.292687 0 0 1 0.7645,0.81452 40.632485,43.292687 0 0 1 0.74449,0.83642 40.632485,43.292687 0 0 1 0.723143,0.85763 40.632485,43.292687 0 0 1 0.70113,0.87754 40.632485,43.292687 0 0 1 0.67912,0.89678 40.632485,43.292687 0 0 1 0.657112,0.916 40.632485,43.292687 0 0 1 0.63307,0.93392 40.632485,43.292687 0 0 1 0.6104,0.95183 40.632485,43.292687 0 0 1 0.58573,0.96907 40.632485,43.292687 0 0 1 0.56171,0.98499 40.632485,43.292687 0 0 1 0.53634,1.00091 40.632485,43.292687 0 0 1 0.51167,1.0155 40.632485,43.292687 0 0 1 0.48631,1.0301 40.632485,43.292687 0 0 1 0.45965,1.04269 40.632485,43.292687 0 0 1 0.43428,1.05597 40.632485,43.292687 0 0 1 0.40694,1.0679 40.632485,43.292687 0 0 1 0.38025,1.07852 40.632485,43.292687 0 0 1 0.35357,1.08979 40.632485,43.292687 0 0 1 0.32622,1.09908 40.632485,43.292687 0 0 1 0.29818,1.10769 40.632485,43.292687 0 0 1 0.27085,1.11566 40.632485,43.292687 0 0 1 0.24283,1.12362 40.632485,43.292687 0 0 1 0.2148,1.12959 40.632485,43.292687 0 0 1 0.18612,1.1349 40.632485,43.292687 0 0 1 0.15745,1.1402 40.632485,43.292687 0 0 1 0.12941,1.14485 40.632485,43.292687 0 0 1 0.10078,1.14749 40.632485,43.292687 0 0 1 0.072,1.15015 40.632485,43.292687 0 0 1 0.0434,1.15148 40.632485,43.292687 0 0 1 0.014,1.15214 40.632485,43.292687 0 0 1 -0.12477,3.39672 40.632485,43.292687 0 0 1 -0.37557,3.37617 40.632485,43.292687 0 0 1 -0.6224,3.33371 40.632485,43.292687 0 0 1 -0.86591,3.27203 40.632485,43.292687 0 0 1 -1.10406,3.18911 40.632485,43.292687 0 0 1 -1.33555,3.08697 40.632485,43.292687 0 0 1 -1.55903,2.96559 40.632485,43.292687 0 0 1 -1.77249,2.82696 40.632485,43.292687 0 0 1 -1.975302,2.66909 40.632485,43.292687 0 0 1 -2.165443,2.49664 40.632485,43.292687 0 0 1 -2.34287,2.30693 40.632485,43.292687 0 0 1 -2.50564,2.10463 40.632485,43.292687 0 0 1 -2.65309,1.88906 40.632485,43.292687 0 0 1 -2.78317,1.66089 40.632485,43.292687 0 0 1 -2.89791,1.42277 40.632485,43.292687 0 0 1 -2.992636,1.17668 40.632485,43.292687 0 0 1 -3.070676,0.92264 40.632485,43.292687 0 0 1 -3.129402,0.66329 40.632485,43.292687 0 0 1 -3.168078,0.3993 40.632485,43.292687 0 0 1 -3.188104,0.13399 40.632485,43.292687 0 0 1 -3.188094,-0.13399 40.632485,43.292687 0 0 1 -3.168088,-0.3993 40.632485,43.292687 0 0 1 -3.12939,-0.6633 40.632485,43.292687 0 0 1 -3.07069,-0.92264 40.632485,43.292687 0 0 1 -2.993303,-1.17668 40.632485,43.292687 0 0 1 -2.897244,-1.42277 40.632485,43.292687 0 0 1 -2.783837,-1.66088 40.632485,43.292687 0 0 1 -2.652404,-1.88906 40.632485,43.292687 0 0 1 -2.505648,-2.10463 40.632485,43.292687 0 0 1 -2.342886,-2.30694 40.632485,43.292687 0 0 1 -2.166082,-2.49664 40.632485,43.292687 0 0 1 -1.974639,-2.66909 40.632485,43.292687 0 0 1 -1.772504,-2.82696 40.632485,43.292687 0 0 1 -1.559022,-2.96558 40.632485,43.292687 0 0 1 -1.335549,-3.08697 40.632485,43.292687 0 0 1 -1.104725,-3.18911 40.632485,43.292687 0 0 1 -0.865908,-3.27203 40.632485,43.292687 0 0 1 -0.622408,-3.33371 40.632485,43.292687 0 0 1 -0.374918,-3.37617 40.632485,43.292687 0 0 1 -0.125409,-3.39672 40.632485,43.292687 0 0 1 0.125409,-3.39673 40.632485,43.292687 0 0 1 0.374918,-3.3755 40.632485,43.292687 0 0 1 0.622408,-3.33371 40.632485,43.292687 0 0 1 0.865908,-3.27203 40.632485,43.292687 0 0 1 1.104725,-3.18912 40.632485,43.292687 0 0 1 1.335549,-3.08696 40.632485,43.292687 0 0 1 1.559022,-2.96625 40.632485,43.292687 0 0 1 1.772504,-2.8263 40.632485,43.292687 0 0 1 1.974639,-2.66975 40.632485,43.292687 0 0 1 2.166082,-2.49598 40.632485,43.292687 0 0 1 2.342886,-2.3076 40.632485,43.292687 0 0 1 2.505648,-2.10463 40.632485,43.292687 0 0 1 2.652426,-1.88839 40.632485,43.292687 0 0 1 2.783836,-1.66089 40.632485,43.292687 0 0 1 2.897245,-1.42343 40.632485,43.292687 0 0 1 2.993303,-1.17602 40.632485,43.292687 0 0 1 3.070689,-0.9233 40.632485,43.292687 0 0 1 3.12939,-0.66263 40.632485,43.292687 0 0 1 3.16809,-0.39997 40.632485,43.292687 0 0 1 3.188093,-0.13332 z"
+ clip-path="url(#clipPath20723)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-63-6"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="m 67.929953,110.87572 a 35.545008,38.588202 0 0 0 -35.544717,38.58853 35.545008,38.588202 0 0 0 35.544717,38.58801 35.545008,38.588202 0 0 0 35.545217,-38.58801 35.545008,38.588202 0 0 0 -35.545217,-38.58853 z m -0.07061,4.90891 a 31.151221,33.78691 0 0 1 0.829048,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.827519,0.0595 31.151221,33.78691 0 0 1 0.825964,0.0839 31.151221,33.78691 0 0 1 0.823425,0.10768 31.151221,33.78691 0 0 1 0.820349,0.13096 31.151221,33.78691 0 0 1 0.816775,0.15478 31.151221,33.78691 0 0 1 0.813198,0.17859 31.151221,33.78691 0 0 1 0.808083,0.20189 31.151221,33.78691 0 0 1 0.802953,0.22518 31.151221,33.78691 0 0 1 0.797339,0.24795 31.151221,33.78691 0 0 1 0.790697,0.27125 31.151221,33.78691 0 0 1 0.783536,0.29403 31.151221,33.78691 0 0 1 0.776355,0.31629 31.151221,33.78691 0 0 1 0.768177,0.33854 31.151221,33.78691 0 0 1 0.760022,0.36081 31.151221,33.78691 0 0 1 0.750783,0.38255 31.151221,33.78691 0 0 1 0.740582,0.40428 31.151221,33.78691 0 0 1 0.73085,0.425 31.151221,33.78691 0 0 1 0.7201,0.44621 31.151221,33.78691 0 0 1 0.70885,0.46693 31.151221,33.78691 0 0 1 0.6971,0.48763 31.151221,33.78691 0 0 1 0.68484,0.50678 31.151221,33.78691 0 0 1 0.67254,0.52697 31.151221,33.78691 0 0 1 0.65872,0.54613 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.5834 31.151221,33.78691 0 0 1 0.61678,0.60099 31.151221,33.78691 0 0 1 0.60196,0.61912 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65276 31.151221,33.78691 0 0 1 0.5544,0.66933 31.151221,33.78691 0 0 1 0.53751,0.68485 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71488 31.151221,33.78691 0 0 1 0.48534,0.72886 31.151221,33.78691 0 0 1 0.46798,0.74283 31.151221,33.78691 0 0 1 0.44904,0.75629 31.151221,33.78691 0 0 1 0.43065,0.76872 31.151221,33.78691 0 0 1 0.41119,0.78114 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80392 31.151221,33.78691 0 0 1 0.35239,0.81375 31.151221,33.78691 0 0 1 0.33294,0.82411 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.8417 31.151221,33.78691 0 0 1 0.27107,0.85051 31.151221,33.78691 0 0 1 0.2501,0.85775 31.151221,33.78691 0 0 1 0.2286,0.86448 31.151221,33.78691 0 0 1 0.20765,0.8707 31.151221,33.78691 0 0 1 0.18616,0.8769 31.151221,33.78691 0 0 1 0.16468,0.88157 31.151221,33.78691 0 0 1 0.14268,0.8857 31.151221,33.78691 0 0 1 0.12073,0.88986 31.151221,33.78691 0 0 1 0.0992,0.89346 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.65091 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60172 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40917 31.151221,33.78691 0 0 1 -1.19526,2.31443 31.151221,33.78691 0 0 1 -1.3589,2.20624 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64252 31.151221,33.78691 0 0 1 -2.034,1.47428 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.221695,1.11038 31.151221,33.78691 0 0 1 -2.294328,0.91831 31.151221,33.78691 0 0 1 -2.354157,0.72006 31.151221,33.78691 0 0 1 -2.399184,0.51765 31.151221,33.78691 0 0 1 -2.428831,0.31163 31.151221,33.78691 0 0 1 -2.444189,0.10457 31.151221,33.78691 0 0 1 -2.44418,-0.10457 31.151221,33.78691 0 0 1 -2.428838,-0.31162 31.151221,33.78691 0 0 1 -2.399177,-0.51765 31.151221,33.78691 0 0 1 -2.354163,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91832 31.151221,33.78691 0 0 1 -2.221196,-1.11037 31.151221,33.78691 0 0 1 -2.134249,-1.29621 31.151221,33.78691 0 0 1 -2.033489,-1.47428 31.151221,33.78691 0 0 1 -1.920973,-1.64251 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20625 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.6509 31.151221,33.78691 0 0 1 0.09615,-2.65091 31.151221,33.78691 0 0 1 0.287434,-2.63434 31.151221,33.78691 0 0 1 0.477175,-2.60173 31.151221,33.78691 0 0 1 0.663854,-2.55359 31.151221,33.78691 0 0 1 0.846946,-2.48888 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31496 31.151221,33.78691 0 0 1 1.358905,-2.20572 31.151221,33.78691 0 0 1 1.513873,-2.08356 31.151221,33.78691 0 0 1 1.660643,-1.94793 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920973,-1.64252 31.151221,33.78691 0 0 1 2.033505,-1.47376 31.151221,33.78691 0 0 1 2.13425,-1.2962 31.151221,33.78691 0 0 1 2.221195,-1.11089 31.151221,33.78691 0 0 1 2.294841,-0.9178 31.151221,33.78691 0 0 1 2.354165,-0.72057 31.151221,33.78691 0 0 1 2.399176,-0.51714 31.151221,33.78691 0 0 1 2.428838,-0.31215 31.151221,33.78691 0 0 1 2.44418,-0.10404 z"
+ clip-path="url(#clipPath20719)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306"
+ style="opacity:1;fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:#df373a;stroke-width:0.0376767;stroke-linejoin:round;stroke-opacity:1"
+ d="m 90.379694,105.31927 a 40.722405,43.678338 0 0 0 -40.72207,43.67871 40.722405,43.678338 0 0 0 40.72207,43.67812 40.722405,43.678338 0 0 0 40.722646,-43.67812 40.722405,43.678338 0 0 0 -40.722646,-43.67871 z m -0.0809,5.55644 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.94628,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12188 35.68863,38.243712 0 0 1 0.93984,0.14824 35.68863,38.243712 0 0 1 0.93574,0.1752 35.68863,38.243712 0 0 1 0.93165,0.20214 35.68863,38.243712 0 0 1 0.925784,0.22852 35.68863,38.243712 0 0 1 0.91992,0.25488 35.68863,38.243712 0 0 1 0.913482,0.28067 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33281 35.68863,38.243712 0 0 1 0.88945,0.35801 35.68863,38.243712 0 0 1 0.88007,0.3832 35.68863,38.243712 0 0 1 0.87071,0.4084 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52851 35.68863,38.243712 0 0 1 0.79863,0.55196 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59648 35.68863,38.243712 0 0 1 0.75469,0.61817 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68027 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71953 35.68863,38.243712 0 0 1 0.65391,0.73887 35.68863,38.243712 0 0 1 0.63515,0.75762 35.68863,38.243712 0 0 1 0.61582,0.77519 35.68863,38.243712 0 0 1 0.59649,0.79219 35.68863,38.243712 0 0 1 0.57715,0.80918 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84082 35.68863,38.243712 0 0 1 0.51446,0.85606 35.68863,38.243712 0 0 1 0.49336,0.87011 35.68863,38.243712 0 0 1 0.47109,0.88418 35.68863,38.243712 0 0 1 0.44941,0.89707 35.68863,38.243712 0 0 1 0.42715,0.90996 35.68863,38.243712 0 0 1 0.40371,0.9211 35.68863,38.243712 0 0 1 0.38145,0.93281 35.68863,38.243712 0 0 1 0.35742,0.94336 35.68863,38.243712 0 0 1 0.33399,0.95273 35.68863,38.243712 0 0 1 0.31054,0.9627 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97851 35.68863,38.243712 0 0 1 0.23789,0.98555 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99785 35.68863,38.243712 0 0 1 0.16348,1.00254 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01132 35.68863,38.243712 0 0 1 0.0885,1.01368 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01778 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98243 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89043 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.17305,2.72696 35.68863,38.243712 0 0 1 -1.36934,2.61972 35.68863,38.243712 0 0 1 -1.55683,2.49727 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46719 35.68863,38.243712 0 0 1 -2.54531,1.25683 35.68863,38.243712 0 0 1 -2.62851,1.03946 35.68863,38.243712 0 0 1 -2.697062,0.81504 35.68863,38.243712 0 0 1 -2.748644,0.58593 35.68863,38.243712 0 0 1 -2.78261,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58593 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03946 35.68863,38.243712 0 0 1 -2.54473,-1.25683 35.68863,38.243712 0 0 1 -2.44512,-1.46719 35.68863,38.243712 0 0 1 -2.32968,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49727 35.68863,38.243712 0 0 1 -1.36933,-2.61972 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89043 35.68863,38.243712 0 0 1 -0.54668,-2.94492 35.68863,38.243712 0 0 1 -0.3293,-2.98243 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00059 35.68863,38.243712 0 0 1 0.3293,-2.98184 35.68863,38.243712 0 0 1 0.54668,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81719 35.68863,38.243712 0 0 1 1.17305,-2.72695 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20489 35.68863,38.243712 0 0 1 2.05782,-2.03847 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.32968,-1.66817 35.68863,38.243712 0 0 1 2.44512,-1.46718 35.68863,38.243712 0 0 1 2.54473,-1.25743 35.68863,38.243712 0 0 1 2.6291,-1.03886 35.68863,38.243712 0 0 1 2.69707,-0.81563 35.68863,38.243712 0 0 1 2.74863,-0.58535 35.68863,38.243712 0 0 1 2.78262,-0.35332 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20715)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0427732;stroke-linejoin:round;stroke-opacity:1"
+ d="M 90.531339,99.785944 A 46.363577,49.444797 0 0 0 44.168144,149.23116 46.363577,49.444797 0 0 0 90.531339,198.67572 46.363577,49.444797 0 0 0 136.89519,149.23116 46.363577,49.444797 0 0 0 90.531339,99.785944 Z m -0.09212,6.290016 a 40.632485,43.292687 0 0 1 1.081384,0.0153 40.632485,43.292687 0 0 1 1.080714,0.0464 40.632485,43.292687 0 0 1 1.079381,0.0763 40.632485,43.292687 0 0 1 1.077365,0.10745 40.632485,43.292687 0 0 1 1.074041,0.13797 40.632485,43.292687 0 0 1 1.070034,0.16781 40.632485,43.292687 0 0 1 1.065366,0.19832 40.632485,43.292687 0 0 1 1.060704,0.22884 40.632485,43.292687 0 0 1 1.054032,0.25868 40.632485,43.292687 0 0 1 1.04736,0.28854 40.632485,43.292687 0 0 1 1.04002,0.31772 40.632485,43.292687 0 0 1 1.03135,0.34756 40.632485,43.292687 0 0 1 1.02201,0.37675 40.632485,43.292687 0 0 1 1.01266,0.40527 40.632485,43.292687 0 0 1 1.00198,0.4338 40.632485,43.292687 0 0 1 0.99134,0.46231 40.632485,43.292687 0 0 1 0.97929,0.49018 40.632485,43.292687 0 0 1 0.96598,0.51803 40.632485,43.292687 0 0 1 0.9533,0.54456 40.632485,43.292687 0 0 1 0.93927,0.57176 40.632485,43.292687 0 0 1 0.92461,0.59829 40.632485,43.292687 0 0 1 0.90926,0.62483 40.632485,43.292687 0 0 1 0.89328,0.64936 40.632485,43.292687 0 0 1 0.87724,0.67523 40.632485,43.292687 0 0 1 0.85922,0.69978 40.632485,43.292687 0 0 1 0.8419,0.72365 40.632485,43.292687 0 0 1 0.82388,0.74753 40.632485,43.292687 0 0 1 0.80452,0.77009 40.632485,43.292687 0 0 1 0.78518,0.7933 40.632485,43.292687 0 0 1 0.7645,0.81452 40.632485,43.292687 0 0 1 0.74449,0.83642 40.632485,43.292687 0 0 1 0.72314,0.85763 40.632485,43.292687 0 0 1 0.70113,0.87754 40.632485,43.292687 0 0 1 0.67912,0.89677 40.632485,43.292687 0 0 1 0.65711,0.91601 40.632485,43.292687 0 0 1 0.63307,0.93392 40.632485,43.292687 0 0 1 0.6104,0.95183 40.632485,43.292687 0 0 1 0.58573,0.96907 40.632485,43.292687 0 0 1 0.56171,0.98499 40.632485,43.292687 0 0 1 0.53634,1.00091 40.632485,43.292687 0 0 1 0.51167,1.0155 40.632485,43.292687 0 0 1 0.48631,1.0301 40.632485,43.292687 0 0 1 0.45965,1.04269 40.632485,43.292687 0 0 1 0.43428,1.05597 40.632485,43.292687 0 0 1 0.40694,1.0679 40.632485,43.292687 0 0 1 0.38025,1.07851 40.632485,43.292687 0 0 1 0.35357,1.0898 40.632485,43.292687 0 0 1 0.32622,1.09908 40.632485,43.292687 0 0 1 0.29818,1.10769 40.632485,43.292687 0 0 1 0.27085,1.11566 40.632485,43.292687 0 0 1 0.24283,1.12362 40.632485,43.292687 0 0 1 0.2148,1.12959 40.632485,43.292687 0 0 1 0.18612,1.1349 40.632485,43.292687 0 0 1 0.15745,1.1402 40.632485,43.292687 0 0 1 0.12941,1.14484 40.632485,43.292687 0 0 1 0.10078,1.1475 40.632485,43.292687 0 0 1 0.072,1.15015 40.632485,43.292687 0 0 1 0.0434,1.15148 40.632485,43.292687 0 0 1 0.014,1.15214 40.632485,43.292687 0 0 1 -0.12477,3.39673 40.632485,43.292687 0 0 1 -0.37557,3.37616 40.632485,43.292687 0 0 1 -0.6224,3.33372 40.632485,43.292687 0 0 1 -0.86591,3.27203 40.632485,43.292687 0 0 1 -1.10406,3.18911 40.632485,43.292687 0 0 1 -1.33555,3.08697 40.632485,43.292687 0 0 1 -1.55903,2.96558 40.632485,43.292687 0 0 1 -1.77249,2.82696 40.632485,43.292687 0 0 1 -1.9753,2.66909 40.632485,43.292687 0 0 1 -2.16544,2.49664 40.632485,43.292687 0 0 1 -2.34287,2.30693 40.632485,43.292687 0 0 1 -2.50564,2.10464 40.632485,43.292687 0 0 1 -2.65309,1.88906 40.632485,43.292687 0 0 1 -2.78317,1.66088 40.632485,43.292687 0 0 1 -2.89791,1.42277 40.632485,43.292687 0 0 1 -2.99264,1.17668 40.632485,43.292687 0 0 1 -3.070682,0.92264 40.632485,43.292687 0 0 1 -3.129401,0.6633 40.632485,43.292687 0 0 1 -3.168078,0.3993 40.632485,43.292687 0 0 1 -3.188104,0.13399 40.632485,43.292687 0 0 1 -3.188094,-0.13399 40.632485,43.292687 0 0 1 -3.168088,-0.3993 40.632485,43.292687 0 0 1 -3.12939,-0.6633 40.632485,43.292687 0 0 1 -3.07069,-0.92264 40.632485,43.292687 0 0 1 -2.993303,-1.17668 40.632485,43.292687 0 0 1 -2.897244,-1.42277 40.632485,43.292687 0 0 1 -2.783837,-1.66088 40.632485,43.292687 0 0 1 -2.652404,-1.88906 40.632485,43.292687 0 0 1 -2.505648,-2.10464 40.632485,43.292687 0 0 1 -2.342886,-2.30693 40.632485,43.292687 0 0 1 -2.166082,-2.49664 40.632485,43.292687 0 0 1 -1.974639,-2.66909 40.632485,43.292687 0 0 1 -1.772504,-2.82696 40.632485,43.292687 0 0 1 -1.559022,-2.96558 40.632485,43.292687 0 0 1 -1.335549,-3.08697 40.632485,43.292687 0 0 1 -1.104725,-3.18911 40.632485,43.292687 0 0 1 -0.865908,-3.27203 40.632485,43.292687 0 0 1 -0.622408,-3.33372 40.632485,43.292687 0 0 1 -0.374918,-3.37616 40.632485,43.292687 0 0 1 -0.125409,-3.39673 40.632485,43.292687 0 0 1 0.125409,-3.39673 40.632485,43.292687 0 0 1 0.374918,-3.3755 40.632485,43.292687 0 0 1 0.622408,-3.33371 40.632485,43.292687 0 0 1 0.865908,-3.27203 40.632485,43.292687 0 0 1 1.104725,-3.18911 40.632485,43.292687 0 0 1 1.335549,-3.08697 40.632485,43.292687 0 0 1 1.559022,-2.96625 40.632485,43.292687 0 0 1 1.772504,-2.82629 40.632485,43.292687 0 0 1 1.974639,-2.66976 40.632485,43.292687 0 0 1 2.166082,-2.49597 40.632485,43.292687 0 0 1 2.342886,-2.3076 40.632485,43.292687 0 0 1 2.505648,-2.10463 40.632485,43.292687 0 0 1 2.652426,-1.8884 40.632485,43.292687 0 0 1 2.783836,-1.66089 40.632485,43.292687 0 0 1 2.897245,-1.42343 40.632485,43.292687 0 0 1 2.993303,-1.17601 40.632485,43.292687 0 0 1 3.070689,-0.92331 40.632485,43.292687 0 0 1 3.12939,-0.66263 40.632485,43.292687 0 0 1 3.16809,-0.39997 40.632485,43.292687 0 0 1 3.188093,-0.13332 z"
+ clip-path="url(#clipPath20711)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-63"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="m 90.144156,110.56416 a 35.545008,38.588202 0 0 0 -35.544717,38.58853 35.545008,38.588202 0 0 0 35.544717,38.58802 35.545008,38.588202 0 0 0 35.545224,-38.58802 35.545008,38.588202 0 0 0 -35.545224,-38.58853 z m -0.07061,4.90892 a 31.151221,33.78691 0 0 1 0.829048,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.827519,0.0595 31.151221,33.78691 0 0 1 0.825964,0.0839 31.151221,33.78691 0 0 1 0.823425,0.10767 31.151221,33.78691 0 0 1 0.820349,0.13097 31.151221,33.78691 0 0 1 0.816775,0.15478 31.151221,33.78691 0 0 1 0.813198,0.17859 31.151221,33.78691 0 0 1 0.808083,0.20188 31.151221,33.78691 0 0 1 0.802953,0.22518 31.151221,33.78691 0 0 1 0.797338,0.24796 31.151221,33.78691 0 0 1 0.7907,0.27125 31.151221,33.78691 0 0 1 0.783542,0.29403 31.151221,33.78691 0 0 1 0.77635,0.31629 31.151221,33.78691 0 0 1 0.76818,0.33854 31.151221,33.78691 0 0 1 0.76002,0.3608 31.151221,33.78691 0 0 1 0.75079,0.38255 31.151221,33.78691 0 0 1 0.74058,0.40429 31.151221,33.78691 0 0 1 0.73085,0.42499 31.151221,33.78691 0 0 1 0.7201,0.44622 31.151221,33.78691 0 0 1 0.70885,0.46692 31.151221,33.78691 0 0 1 0.6971,0.48764 31.151221,33.78691 0 0 1 0.68484,0.50678 31.151221,33.78691 0 0 1 0.67254,0.52697 31.151221,33.78691 0 0 1 0.65872,0.54613 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.58339 31.151221,33.78691 0 0 1 0.61678,0.601 31.151221,33.78691 0 0 1 0.60196,0.61911 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65277 31.151221,33.78691 0 0 1 0.5544,0.66932 31.151221,33.78691 0 0 1 0.53751,0.68486 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71488 31.151221,33.78691 0 0 1 0.48534,0.72885 31.151221,33.78691 0 0 1 0.46798,0.74284 31.151221,33.78691 0 0 1 0.44904,0.75629 31.151221,33.78691 0 0 1 0.43065,0.76872 31.151221,33.78691 0 0 1 0.41119,0.78114 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80391 31.151221,33.78691 0 0 1 0.35239,0.81376 31.151221,33.78691 0 0 1 0.33294,0.8241 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.84171 31.151221,33.78691 0 0 1 0.27107,0.85051 31.151221,33.78691 0 0 1 0.2501,0.85775 31.151221,33.78691 0 0 1 0.2286,0.86448 31.151221,33.78691 0 0 1 0.20765,0.8707 31.151221,33.78691 0 0 1 0.18616,0.8769 31.151221,33.78691 0 0 1 0.16468,0.88157 31.151221,33.78691 0 0 1 0.14268,0.8857 31.151221,33.78691 0 0 1 0.12073,0.88985 31.151221,33.78691 0 0 1 0.0992,0.89347 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.6509 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60173 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40916 31.151221,33.78691 0 0 1 -1.19526,2.31444 31.151221,33.78691 0 0 1 -1.3589,2.20624 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64252 31.151221,33.78691 0 0 1 -2.034,1.47428 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.2217,1.11037 31.151221,33.78691 0 0 1 -2.294332,0.91832 31.151221,33.78691 0 0 1 -2.354157,0.72006 31.151221,33.78691 0 0 1 -2.399184,0.51765 31.151221,33.78691 0 0 1 -2.428831,0.31163 31.151221,33.78691 0 0 1 -2.444189,0.10457 31.151221,33.78691 0 0 1 -2.44418,-0.10457 31.151221,33.78691 0 0 1 -2.428838,-0.31163 31.151221,33.78691 0 0 1 -2.399177,-0.51765 31.151221,33.78691 0 0 1 -2.354163,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91831 31.151221,33.78691 0 0 1 -2.221196,-1.11037 31.151221,33.78691 0 0 1 -2.134249,-1.29621 31.151221,33.78691 0 0 1 -2.033489,-1.47428 31.151221,33.78691 0 0 1 -1.920973,-1.64252 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20624 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.65091 31.151221,33.78691 0 0 1 0.09615,-2.65091 31.151221,33.78691 0 0 1 0.287434,-2.63433 31.151221,33.78691 0 0 1 0.477175,-2.60174 31.151221,33.78691 0 0 1 0.663854,-2.55358 31.151221,33.78691 0 0 1 0.846946,-2.48889 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31495 31.151221,33.78691 0 0 1 1.358905,-2.20572 31.151221,33.78691 0 0 1 1.513873,-2.08356 31.151221,33.78691 0 0 1 1.660643,-1.94793 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920973,-1.64252 31.151221,33.78691 0 0 1 2.033505,-1.47376 31.151221,33.78691 0 0 1 2.13425,-1.29621 31.151221,33.78691 0 0 1 2.221195,-1.11088 31.151221,33.78691 0 0 1 2.294841,-0.9178 31.151221,33.78691 0 0 1 2.354165,-0.72058 31.151221,33.78691 0 0 1 2.399176,-0.51713 31.151221,33.78691 0 0 1 2.428838,-0.31215 31.151221,33.78691 0 0 1 2.44418,-0.10405 z"
+ clip-path="url(#clipPath20707)"
+ transform="translate(-177.74838,-111.36079)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 119.20127,221.87113 c 15.58969,0 29.12922,9.40117 35.96102,23.20181 h -5.81736 c -6.31922,-10.6997 -17.45681,-17.80491 -30.14366,-17.80491 -19.690574,0 -35.652879,17.112 -35.652879,38.22056 0,10.3318 3.825597,19.70468 10.03957,26.58295 -1.342357,1.12091 -2.771532,2.1279 -4.275488,3.00675 -6.701874,-7.77494 -10.798502,-18.16843 -10.798502,-29.5897 0,-24.08921 18.216325,-43.61746 40.687299,-43.61746 z m 35.852,64.25471 c -6.86645,13.68013 -20.34561,22.98022 -35.852,22.98022 -1.0527,0 -2.0961,-0.0429 -3.12869,-0.12703 3.0522,-1.56117 5.91359,-3.48039 8.53831,-5.70731 10.32096,-1.68438 19.18598,-8.11363 24.60181,-17.14588 z"
+ id="path2350-0"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943"
+ clip-path="url(#clipPath20703)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 96.751486,221.87113 c 1.052607,0 2.095998,0.0429 3.128684,0.12706 -3.052192,1.56117 -5.913678,3.48036 -8.538403,5.70731 -17.123111,2.7943 -30.243159,18.646 -30.243159,37.78309 0,14.26457 7.29059,26.70203 18.093843,33.26893 -1.593656,0.26719 -3.226966,0.40695 -4.890748,0.40695 -1.239545,0 -2.46151,-0.0795 -3.663522,-0.22937 -8.907938,-8.00114 -14.573991,-20.01353 -14.573991,-33.44651 0,-24.08921 18.216324,-43.61746 40.687296,-43.61746 z m 5.409714,81.40059 c 10.32112,-1.68438 19.18608,-8.11394 24.60197,-17.14636 h 5.84051 c -6.86635,13.68031 -20.34554,22.9807 -35.852194,22.9807 -1.052703,0 -2.095999,-0.0429 -3.128684,-0.12703 3.052002,-1.56137 5.913836,-3.48022 8.538398,-5.70731 z m 24.7338,-58.19878 c -3.13939,-5.31472 -7.46755,-9.74275 -12.58451,-12.85327 1.59365,-0.26719 3.2269,-0.40695 4.89078,-0.40695 1.23945,0 2.46151,0.0795 3.66352,0.22937 4.01602,3.60724 7.3732,8.03011 9.84905,13.03085 z"
+ id="path2352-7"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943"
+ clip-path="url(#clipPath20699)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 74.301703,221.87113 c 1.064296,0 2.118804,0.0444 3.162607,0.13022 -3.046523,1.55896 -5.903162,3.47451 -8.52358,5.69681 -17.146992,2.77237 -30.291903,18.63524 -30.291903,37.79043 0,21.10857 15.962401,38.22057 35.652876,38.22057 12.599746,0 23.672446,-7.00705 30.013747,-17.5838 h 5.83852 c -6.86636,13.68031 -20.345616,22.9807 -35.852267,22.9807 -22.470907,0 -40.6872,-19.52825 -40.6872,-43.61747 0,-24.08921 18.216293,-43.61746 40.6872,-43.61746 z m 30.142787,23.20181 c -1.31192,-2.22057 -2.83098,-4.28705 -4.528878,-6.16651 1.342448,-1.12094 2.771378,-2.12838 4.275138,-3.00723 2.37299,2.75301 4.41888,5.83464 6.07249,9.17374 z"
+ id="path2354-0"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943"
+ clip-path="url(#clipPath20695)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-9"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0376767;stroke-linejoin:round;stroke-opacity:1"
+ d="m 119.12038,221.87113 a 40.722405,43.678338 0 0 0 -40.722073,43.67871 40.722405,43.678338 0 0 0 40.722073,43.67813 40.722405,43.678338 0 0 0 40.72263,-43.67813 40.722405,43.678338 0 0 0 -40.72263,-43.67871 z m -0.0809,5.55645 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.94628,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12187 35.68863,38.243712 0 0 1 0.93984,0.14824 35.68863,38.243712 0 0 1 0.93574,0.1752 35.68863,38.243712 0 0 1 0.93165,0.20215 35.68863,38.243712 0 0 1 0.92578,0.22851 35.68863,38.243712 0 0 1 0.91992,0.25489 35.68863,38.243712 0 0 1 0.91348,0.28066 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33281 35.68863,38.243712 0 0 1 0.88945,0.35801 35.68863,38.243712 0 0 1 0.88006,0.38321 35.68863,38.243712 0 0 1 0.87071,0.40839 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52852 35.68863,38.243712 0 0 1 0.79863,0.55195 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59649 35.68863,38.243712 0 0 1 0.75469,0.61816 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68027 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71954 35.68863,38.243712 0 0 1 0.65391,0.73886 35.68863,38.243712 0 0 1 0.63515,0.75762 35.68863,38.243712 0 0 1 0.61582,0.7752 35.68863,38.243712 0 0 1 0.59649,0.79218 35.68863,38.243712 0 0 1 0.57715,0.80918 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84082 35.68863,38.243712 0 0 1 0.51446,0.85606 35.68863,38.243712 0 0 1 0.49336,0.87012 35.68863,38.243712 0 0 1 0.47109,0.88417 35.68863,38.243712 0 0 1 0.44941,0.89708 35.68863,38.243712 0 0 1 0.42715,0.90996 35.68863,38.243712 0 0 1 0.40371,0.92109 35.68863,38.243712 0 0 1 0.38145,0.93281 35.68863,38.243712 0 0 1 0.35742,0.94336 35.68863,38.243712 0 0 1 0.33399,0.95274 35.68863,38.243712 0 0 1 0.31054,0.96269 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97852 35.68863,38.243712 0 0 1 0.23789,0.98554 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99785 35.68863,38.243712 0 0 1 0.16348,1.00254 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01133 35.68863,38.243712 0 0 1 0.0885,1.01367 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01778 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98242 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89044 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.17305,2.72696 35.68863,38.243712 0 0 1 -1.36934,2.61972 35.68863,38.243712 0 0 1 -1.55683,2.49727 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46718 35.68863,38.243712 0 0 1 -2.54531,1.25684 35.68863,38.243712 0 0 1 -2.6285,1.03945 35.68863,38.243712 0 0 1 -2.69706,0.81504 35.68863,38.243712 0 0 1 -2.74864,0.58594 35.68863,38.243712 0 0 1 -2.78261,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58594 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03945 35.68863,38.243712 0 0 1 -2.54473,-1.25684 35.68863,38.243712 0 0 1 -2.44512,-1.46718 35.68863,38.243712 0 0 1 -2.329683,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49727 35.68863,38.243712 0 0 1 -1.36933,-2.61972 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89044 35.68863,38.243712 0 0 1 -0.54668,-2.94492 35.68863,38.243712 0 0 1 -0.3293,-2.98242 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00059 35.68863,38.243712 0 0 1 0.3293,-2.98184 35.68863,38.243712 0 0 1 0.54668,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81719 35.68863,38.243712 0 0 1 1.17305,-2.72695 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20488 35.68863,38.243712 0 0 1 2.05782,-2.03848 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.329683,-1.66816 35.68863,38.243712 0 0 1 2.44512,-1.46719 35.68863,38.243712 0 0 1 2.54473,-1.25742 35.68863,38.243712 0 0 1 2.6291,-1.03887 35.68863,38.243712 0 0 1 2.69707,-0.81562 35.68863,38.243712 0 0 1 2.74863,-0.58536 35.68863,38.243712 0 0 1 2.78262,-0.35332 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20671)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-63-4"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="m 118.88484,227.11603 a 35.545008,38.588202 0 0 0 -35.544718,38.58853 35.545008,38.588202 0 0 0 35.544718,38.58802 35.545008,38.588202 0 0 0 35.54521,-38.58802 35.545008,38.588202 0 0 0 -35.54521,-38.58853 z m -0.0706,4.90891 a 31.151221,33.78691 0 0 1 0.82905,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.82752,0.0595 31.151221,33.78691 0 0 1 0.82596,0.0839 31.151221,33.78691 0 0 1 0.82342,0.10768 31.151221,33.78691 0 0 1 0.82035,0.13096 31.151221,33.78691 0 0 1 0.81678,0.15478 31.151221,33.78691 0 0 1 0.8132,0.17859 31.151221,33.78691 0 0 1 0.80808,0.20189 31.151221,33.78691 0 0 1 0.80295,0.22518 31.151221,33.78691 0 0 1 0.79734,0.24795 31.151221,33.78691 0 0 1 0.7907,0.27125 31.151221,33.78691 0 0 1 0.78353,0.29403 31.151221,33.78691 0 0 1 0.77636,0.31629 31.151221,33.78691 0 0 1 0.76818,0.33854 31.151221,33.78691 0 0 1 0.76002,0.36081 31.151221,33.78691 0 0 1 0.75078,0.38255 31.151221,33.78691 0 0 1 0.74058,0.40428 31.151221,33.78691 0 0 1 0.73085,0.425 31.151221,33.78691 0 0 1 0.7201,0.44622 31.151221,33.78691 0 0 1 0.70885,0.46692 31.151221,33.78691 0 0 1 0.6971,0.48763 31.151221,33.78691 0 0 1 0.68484,0.50678 31.151221,33.78691 0 0 1 0.67254,0.52698 31.151221,33.78691 0 0 1 0.65872,0.54612 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.5834 31.151221,33.78691 0 0 1 0.61678,0.60099 31.151221,33.78691 0 0 1 0.60196,0.61912 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65276 31.151221,33.78691 0 0 1 0.5544,0.66933 31.151221,33.78691 0 0 1 0.53751,0.68485 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71488 31.151221,33.78691 0 0 1 0.48534,0.72886 31.151221,33.78691 0 0 1 0.46798,0.74284 31.151221,33.78691 0 0 1 0.44904,0.75629 31.151221,33.78691 0 0 1 0.43065,0.76871 31.151221,33.78691 0 0 1 0.41119,0.78114 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80392 31.151221,33.78691 0 0 1 0.35239,0.81375 31.151221,33.78691 0 0 1 0.33294,0.82411 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.8417 31.151221,33.78691 0 0 1 0.27107,0.85051 31.151221,33.78691 0 0 1 0.2501,0.85775 31.151221,33.78691 0 0 1 0.2286,0.86448 31.151221,33.78691 0 0 1 0.20765,0.8707 31.151221,33.78691 0 0 1 0.18616,0.87691 31.151221,33.78691 0 0 1 0.16468,0.88156 31.151221,33.78691 0 0 1 0.14268,0.88571 31.151221,33.78691 0 0 1 0.12073,0.88985 31.151221,33.78691 0 0 1 0.0992,0.89347 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.6509 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60173 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40916 31.151221,33.78691 0 0 1 -1.19526,2.31443 31.151221,33.78691 0 0 1 -1.3589,2.20625 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64251 31.151221,33.78691 0 0 1 -2.034,1.47429 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.22169,1.11037 31.151221,33.78691 0 0 1 -2.29433,0.91832 31.151221,33.78691 0 0 1 -2.35416,0.72006 31.151221,33.78691 0 0 1 -2.39918,0.51765 31.151221,33.78691 0 0 1 -2.42883,0.31163 31.151221,33.78691 0 0 1 -2.44419,0.10456 31.151221,33.78691 0 0 1 -2.44418,-0.10456 31.151221,33.78691 0 0 1 -2.42884,-0.31163 31.151221,33.78691 0 0 1 -2.39918,-0.51765 31.151221,33.78691 0 0 1 -2.35416,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91831 31.151221,33.78691 0 0 1 -2.22119,-1.11038 31.151221,33.78691 0 0 1 -2.13425,-1.2962 31.151221,33.78691 0 0 1 -2.03349,-1.47428 31.151221,33.78691 0 0 1 -1.920975,-1.64252 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20624 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.65091 31.151221,33.78691 0 0 1 0.09615,-2.65091 31.151221,33.78691 0 0 1 0.287434,-2.63434 31.151221,33.78691 0 0 1 0.477175,-2.60173 31.151221,33.78691 0 0 1 0.663854,-2.55359 31.151221,33.78691 0 0 1 0.846946,-2.48888 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31495 31.151221,33.78691 0 0 1 1.358905,-2.20573 31.151221,33.78691 0 0 1 1.513873,-2.08355 31.151221,33.78691 0 0 1 1.660643,-1.94794 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920975,-1.64251 31.151221,33.78691 0 0 1 2.0335,-1.47377 31.151221,33.78691 0 0 1 2.13425,-1.2962 31.151221,33.78691 0 0 1 2.2212,-1.11089 31.151221,33.78691 0 0 1 2.29484,-0.9178 31.151221,33.78691 0 0 1 2.35416,-0.72057 31.151221,33.78691 0 0 1 2.39918,-0.51714 31.151221,33.78691 0 0 1 2.42884,-0.31215 31.151221,33.78691 0 0 1 2.44418,-0.10404 z"
+ clip-path="url(#clipPath20663)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-0-7"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0376767;stroke-linejoin:round"
+ d="m 96.751486,221.87113 a 40.722405,43.678338 0 0 0 -40.72207,43.67871 40.722405,43.678338 0 0 0 40.72207,43.67813 40.722405,43.678338 0 0 0 40.722634,-43.67813 40.722405,43.678338 0 0 0 -40.722634,-43.67871 z m -0.0809,5.55645 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.946284,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12187 35.68863,38.243712 0 0 1 0.93984,0.14825 35.68863,38.243712 0 0 1 0.93574,0.17519 35.68863,38.243712 0 0 1 0.93165,0.20215 35.68863,38.243712 0 0 1 0.92578,0.22851 35.68863,38.243712 0 0 1 0.91992,0.25489 35.68863,38.243712 0 0 1 0.91348,0.28066 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33282 35.68863,38.243712 0 0 1 0.88945,0.358 35.68863,38.243712 0 0 1 0.88006,0.38321 35.68863,38.243712 0 0 1 0.87071,0.40839 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52852 35.68863,38.243712 0 0 1 0.79863,0.55195 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59649 35.68863,38.243712 0 0 1 0.75469,0.61816 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68028 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71953 35.68863,38.243712 0 0 1 0.65391,0.73887 35.68863,38.243712 0 0 1 0.63515,0.75761 35.68863,38.243712 0 0 1 0.61582,0.7752 35.68863,38.243712 0 0 1 0.59649,0.79219 35.68863,38.243712 0 0 1 0.57715,0.80917 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84083 35.68863,38.243712 0 0 1 0.51446,0.85605 35.68863,38.243712 0 0 1 0.49336,0.87012 35.68863,38.243712 0 0 1 0.47109,0.88418 35.68863,38.243712 0 0 1 0.44941,0.89707 35.68863,38.243712 0 0 1 0.42715,0.90996 35.68863,38.243712 0 0 1 0.40371,0.92109 35.68863,38.243712 0 0 1 0.38145,0.93282 35.68863,38.243712 0 0 1 0.35742,0.94335 35.68863,38.243712 0 0 1 0.33399,0.95274 35.68863,38.243712 0 0 1 0.31054,0.96269 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97852 35.68863,38.243712 0 0 1 0.23789,0.98554 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99786 35.68863,38.243712 0 0 1 0.16348,1.00253 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01133 35.68863,38.243712 0 0 1 0.0885,1.01367 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01778 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98243 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89043 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.17305,2.72696 35.68863,38.243712 0 0 1 -1.36934,2.61972 35.68863,38.243712 0 0 1 -1.55683,2.49727 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46719 35.68863,38.243712 0 0 1 -2.54531,1.25683 35.68863,38.243712 0 0 1 -2.6285,1.03946 35.68863,38.243712 0 0 1 -2.69706,0.81504 35.68863,38.243712 0 0 1 -2.74864,0.58593 35.68863,38.243712 0 0 1 -2.782614,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58593 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03946 35.68863,38.243712 0 0 1 -2.54473,-1.25683 35.68863,38.243712 0 0 1 -2.44512,-1.46719 35.68863,38.243712 0 0 1 -2.32968,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49727 35.68863,38.243712 0 0 1 -1.36933,-2.61972 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89043 35.68863,38.243712 0 0 1 -0.54668,-2.94492 35.68863,38.243712 0 0 1 -0.3293,-2.98243 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00059 35.68863,38.243712 0 0 1 0.3293,-2.98184 35.68863,38.243712 0 0 1 0.54668,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81718 35.68863,38.243712 0 0 1 1.17305,-2.72696 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20488 35.68863,38.243712 0 0 1 2.05782,-2.03848 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.32968,-1.66816 35.68863,38.243712 0 0 1 2.44512,-1.46719 35.68863,38.243712 0 0 1 2.54473,-1.25742 35.68863,38.243712 0 0 1 2.6291,-1.03887 35.68863,38.243712 0 0 1 2.69707,-0.81562 35.68863,38.243712 0 0 1 2.74863,-0.58535 35.68863,38.243712 0 0 1 2.78262,-0.35333 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20687)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-63-9-8"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="m 74.220853,227.42758 a 35.545008,38.588202 0 0 0 -35.544717,38.58853 35.545008,38.588202 0 0 0 35.544717,38.58802 35.545008,38.588202 0 0 0 35.545217,-38.58802 35.545008,38.588202 0 0 0 -35.545217,-38.58853 z m -0.07061,4.90891 a 31.151221,33.78691 0 0 1 0.829048,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.827519,0.0595 31.151221,33.78691 0 0 1 0.825964,0.0839 31.151221,33.78691 0 0 1 0.823425,0.10768 31.151221,33.78691 0 0 1 0.820349,0.13096 31.151221,33.78691 0 0 1 0.816775,0.15478 31.151221,33.78691 0 0 1 0.813198,0.17859 31.151221,33.78691 0 0 1 0.808083,0.20189 31.151221,33.78691 0 0 1 0.802953,0.22518 31.151221,33.78691 0 0 1 0.797339,0.24795 31.151221,33.78691 0 0 1 0.790697,0.27125 31.151221,33.78691 0 0 1 0.783536,0.29403 31.151221,33.78691 0 0 1 0.776355,0.31629 31.151221,33.78691 0 0 1 0.768177,0.33854 31.151221,33.78691 0 0 1 0.760022,0.36081 31.151221,33.78691 0 0 1 0.750783,0.38255 31.151221,33.78691 0 0 1 0.740582,0.40428 31.151221,33.78691 0 0 1 0.73085,0.425 31.151221,33.78691 0 0 1 0.7201,0.44622 31.151221,33.78691 0 0 1 0.70885,0.46692 31.151221,33.78691 0 0 1 0.6971,0.48763 31.151221,33.78691 0 0 1 0.68484,0.50678 31.151221,33.78691 0 0 1 0.67254,0.52698 31.151221,33.78691 0 0 1 0.65872,0.54612 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.5834 31.151221,33.78691 0 0 1 0.61678,0.60099 31.151221,33.78691 0 0 1 0.60196,0.61912 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65276 31.151221,33.78691 0 0 1 0.5544,0.66933 31.151221,33.78691 0 0 1 0.53751,0.68485 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71488 31.151221,33.78691 0 0 1 0.48534,0.72886 31.151221,33.78691 0 0 1 0.467982,0.74283 31.151221,33.78691 0 0 1 0.44904,0.7563 31.151221,33.78691 0 0 1 0.43065,0.76871 31.151221,33.78691 0 0 1 0.41119,0.78114 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80392 31.151221,33.78691 0 0 1 0.35239,0.81375 31.151221,33.78691 0 0 1 0.33294,0.82411 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.8417 31.151221,33.78691 0 0 1 0.27107,0.85051 31.151221,33.78691 0 0 1 0.2501,0.85776 31.151221,33.78691 0 0 1 0.2286,0.86447 31.151221,33.78691 0 0 1 0.20765,0.8707 31.151221,33.78691 0 0 1 0.18616,0.87691 31.151221,33.78691 0 0 1 0.16468,0.88156 31.151221,33.78691 0 0 1 0.14268,0.88571 31.151221,33.78691 0 0 1 0.12073,0.88985 31.151221,33.78691 0 0 1 0.0992,0.89347 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.6509 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60173 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40916 31.151221,33.78691 0 0 1 -1.19526,2.31443 31.151221,33.78691 0 0 1 -1.358902,2.20625 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64252 31.151221,33.78691 0 0 1 -2.034,1.47428 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.221695,1.11037 31.151221,33.78691 0 0 1 -2.294328,0.91832 31.151221,33.78691 0 0 1 -2.354157,0.72006 31.151221,33.78691 0 0 1 -2.399184,0.51765 31.151221,33.78691 0 0 1 -2.428831,0.31163 31.151221,33.78691 0 0 1 -2.444189,0.10456 31.151221,33.78691 0 0 1 -2.44418,-0.10456 31.151221,33.78691 0 0 1 -2.428838,-0.31163 31.151221,33.78691 0 0 1 -2.399177,-0.51765 31.151221,33.78691 0 0 1 -2.354163,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91831 31.151221,33.78691 0 0 1 -2.221196,-1.11038 31.151221,33.78691 0 0 1 -2.134249,-1.2962 31.151221,33.78691 0 0 1 -2.033489,-1.47428 31.151221,33.78691 0 0 1 -1.920973,-1.64252 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20624 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.65091 31.151221,33.78691 0 0 1 0.09615,-2.65091 31.151221,33.78691 0 0 1 0.287434,-2.63434 31.151221,33.78691 0 0 1 0.477175,-2.60173 31.151221,33.78691 0 0 1 0.663854,-2.55359 31.151221,33.78691 0 0 1 0.846946,-2.48888 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31495 31.151221,33.78691 0 0 1 1.358905,-2.20573 31.151221,33.78691 0 0 1 1.513873,-2.08355 31.151221,33.78691 0 0 1 1.660643,-1.94794 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920973,-1.64251 31.151221,33.78691 0 0 1 2.033505,-1.47376 31.151221,33.78691 0 0 1 2.13425,-1.29621 31.151221,33.78691 0 0 1 2.221195,-1.11089 31.151221,33.78691 0 0 1 2.294841,-0.9178 31.151221,33.78691 0 0 1 2.354165,-0.72057 31.151221,33.78691 0 0 1 2.399176,-0.51714 31.151221,33.78691 0 0 1 2.428838,-0.31214 31.151221,33.78691 0 0 1 2.44418,-0.10405 z"
+ clip-path="url(#clipPath20691)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-63-6-2"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0330856;stroke-linejoin:round;stroke-opacity:1"
+ d="m 96.670636,227.42758 a 35.545008,38.588202 0 0 0 -35.544717,38.58854 35.545008,38.588202 0 0 0 35.544717,38.58801 35.545008,38.588202 0 0 0 35.545214,-38.58801 35.545008,38.588202 0 0 0 -35.545214,-38.58854 z m -0.07061,4.90892 a 31.151221,33.78691 0 0 1 0.829048,0.0119 31.151221,33.78691 0 0 1 0.82854,0.0362 31.151221,33.78691 0 0 1 0.827519,0.0595 31.151221,33.78691 0 0 1 0.825964,0.0839 31.151221,33.78691 0 0 1 0.823423,0.10767 31.151221,33.78691 0 0 1 0.82035,0.13097 31.151221,33.78691 0 0 1 0.81678,0.15477 31.151221,33.78691 0 0 1 0.81319,0.1786 31.151221,33.78691 0 0 1 0.80809,0.20188 31.151221,33.78691 0 0 1 0.80295,0.22518 31.151221,33.78691 0 0 1 0.79734,0.24796 31.151221,33.78691 0 0 1 0.7907,0.27125 31.151221,33.78691 0 0 1 0.78353,0.29403 31.151221,33.78691 0 0 1 0.77636,0.31628 31.151221,33.78691 0 0 1 0.76817,0.33855 31.151221,33.78691 0 0 1 0.76003,0.3608 31.151221,33.78691 0 0 1 0.75078,0.38255 31.151221,33.78691 0 0 1 0.74058,0.40429 31.151221,33.78691 0 0 1 0.73085,0.42499 31.151221,33.78691 0 0 1 0.7201,0.44622 31.151221,33.78691 0 0 1 0.70885,0.46692 31.151221,33.78691 0 0 1 0.6971,0.48763 31.151221,33.78691 0 0 1 0.68484,0.50679 31.151221,33.78691 0 0 1 0.67254,0.52697 31.151221,33.78691 0 0 1 0.65872,0.54612 31.151221,33.78691 0 0 1 0.64545,0.56476 31.151221,33.78691 0 0 1 0.63165,0.5834 31.151221,33.78691 0 0 1 0.61678,0.601 31.151221,33.78691 0 0 1 0.60196,0.61911 31.151221,33.78691 0 0 1 0.58611,0.63568 31.151221,33.78691 0 0 1 0.57078,0.65276 31.151221,33.78691 0 0 1 0.5544,0.66933 31.151221,33.78691 0 0 1 0.53751,0.68486 31.151221,33.78691 0 0 1 0.52066,0.69987 31.151221,33.78691 0 0 1 0.50379,0.71487 31.151221,33.78691 0 0 1 0.48534,0.72886 31.151221,33.78691 0 0 1 0.46798,0.74284 31.151221,33.78691 0 0 1 0.44904,0.75629 31.151221,33.78691 0 0 1 0.43065,0.76872 31.151221,33.78691 0 0 1 0.41119,0.78113 31.151221,33.78691 0 0 1 0.39227,0.79253 31.151221,33.78691 0 0 1 0.37284,0.80392 31.151221,33.78691 0 0 1 0.35239,0.81375 31.151221,33.78691 0 0 1 0.33294,0.82411 31.151221,33.78691 0 0 1 0.31198,0.83342 31.151221,33.78691 0 0 1 0.29153,0.84171 31.151221,33.78691 0 0 1 0.27107,0.8505 31.151221,33.78691 0 0 1 0.2501,0.85776 31.151221,33.78691 0 0 1 0.2286,0.86448 31.151221,33.78691 0 0 1 0.20765,0.87069 31.151221,33.78691 0 0 1 0.18616,0.87691 31.151221,33.78691 0 0 1 0.16468,0.88157 31.151221,33.78691 0 0 1 0.14268,0.8857 31.151221,33.78691 0 0 1 0.12073,0.88985 31.151221,33.78691 0 0 1 0.0992,0.89347 31.151221,33.78691 0 0 1 0.0772,0.89554 31.151221,33.78691 0 0 1 0.0553,0.89761 31.151221,33.78691 0 0 1 0.0332,0.89865 31.151221,33.78691 0 0 1 0.0108,0.89917 31.151221,33.78691 0 0 1 -0.0957,2.6509 31.151221,33.78691 0 0 1 -0.28795,2.63486 31.151221,33.78691 0 0 1 -0.47716,2.60173 31.151221,33.78691 0 0 1 -0.66385,2.55359 31.151221,33.78691 0 0 1 -0.84644,2.48888 31.151221,33.78691 0 0 1 -1.0239,2.40917 31.151221,33.78691 0 0 1 -1.19526,2.31443 31.151221,33.78691 0 0 1 -1.3589,2.20624 31.151221,33.78691 0 0 1 -1.51437,2.08304 31.151221,33.78691 0 0 1 -1.66015,1.94845 31.151221,33.78691 0 0 1 -1.79618,1.8004 31.151221,33.78691 0 0 1 -1.92098,1.64252 31.151221,33.78691 0 0 1 -2.034,1.47428 31.151221,33.78691 0 0 1 -2.13375,1.2962 31.151221,33.78691 0 0 1 -2.22169,1.11037 31.151221,33.78691 0 0 1 -2.29433,0.91832 31.151221,33.78691 0 0 1 -2.35416,0.72006 31.151221,33.78691 0 0 1 -2.39918,0.51765 31.151221,33.78691 0 0 1 -2.428834,0.31163 31.151221,33.78691 0 0 1 -2.444189,0.10457 31.151221,33.78691 0 0 1 -2.44418,-0.10457 31.151221,33.78691 0 0 1 -2.428838,-0.31162 31.151221,33.78691 0 0 1 -2.399177,-0.51766 31.151221,33.78691 0 0 1 -2.354163,-0.72006 31.151221,33.78691 0 0 1 -2.29484,-0.91831 31.151221,33.78691 0 0 1 -2.221196,-1.11037 31.151221,33.78691 0 0 1 -2.134249,-1.29621 31.151221,33.78691 0 0 1 -2.033489,-1.47428 31.151221,33.78691 0 0 1 -1.920973,-1.64251 31.151221,33.78691 0 0 1 -1.796194,-1.8004 31.151221,33.78691 0 0 1 -1.660643,-1.94845 31.151221,33.78691 0 0 1 -1.513873,-2.08304 31.151221,33.78691 0 0 1 -1.358905,-2.20625 31.151221,33.78691 0 0 1 -1.195236,-2.31443 31.151221,33.78691 0 0 1 -1.023911,-2.40916 31.151221,33.78691 0 0 1 -0.846946,-2.48888 31.151221,33.78691 0 0 1 -0.663854,-2.55359 31.151221,33.78691 0 0 1 -0.477175,-2.60173 31.151221,33.78691 0 0 1 -0.287434,-2.63486 31.151221,33.78691 0 0 1 -0.09615,-2.65091 31.151221,33.78691 0 0 1 0.09615,-2.6509 31.151221,33.78691 0 0 1 0.287434,-2.63435 31.151221,33.78691 0 0 1 0.477175,-2.60173 31.151221,33.78691 0 0 1 0.663854,-2.55359 31.151221,33.78691 0 0 1 0.846946,-2.48888 31.151221,33.78691 0 0 1 1.023911,-2.40916 31.151221,33.78691 0 0 1 1.195236,-2.31495 31.151221,33.78691 0 0 1 1.358905,-2.20572 31.151221,33.78691 0 0 1 1.513873,-2.08356 31.151221,33.78691 0 0 1 1.660643,-1.94793 31.151221,33.78691 0 0 1 1.796194,-1.80092 31.151221,33.78691 0 0 1 1.920973,-1.64252 31.151221,33.78691 0 0 1 2.033505,-1.47376 31.151221,33.78691 0 0 1 2.13425,-1.29621 31.151221,33.78691 0 0 1 2.221195,-1.11088 31.151221,33.78691 0 0 1 2.294841,-0.9178 31.151221,33.78691 0 0 1 2.354165,-0.72058 31.151221,33.78691 0 0 1 2.399176,-0.51714 31.151221,33.78691 0 0 1 2.428838,-0.31214 31.151221,33.78691 0 0 1 2.44418,-0.10405 z"
+ clip-path="url(#clipPath20675)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-6-6"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0427732;stroke-linejoin:round;stroke-opacity:1"
+ d="M 96.843606,215.58112 A 46.363577,49.444797 0 0 0 50.480411,265.02634 46.363577,49.444797 0 0 0 96.843606,314.4709 46.363577,49.444797 0 0 0 143.20744,265.02634 46.363577,49.444797 0 0 0 96.843606,215.58112 Z m -0.09212,6.29001 a 40.632485,43.292687 0 0 1 1.081384,0.0153 40.632485,43.292687 0 0 1 1.080714,0.0464 40.632485,43.292687 0 0 1 1.079381,0.0763 40.632485,43.292687 0 0 1 1.077365,0.10746 40.632485,43.292687 0 0 1 1.07404,0.13796 40.632485,43.292687 0 0 1 1.07004,0.16781 40.632485,43.292687 0 0 1 1.06536,0.19833 40.632485,43.292687 0 0 1 1.06071,0.22884 40.632485,43.292687 0 0 1 1.05403,0.25868 40.632485,43.292687 0 0 1 1.04735,0.28853 40.632485,43.292687 0 0 1 1.04002,0.31772 40.632485,43.292687 0 0 1 1.03135,0.34757 40.632485,43.292687 0 0 1 1.02201,0.37675 40.632485,43.292687 0 0 1 1.01266,0.40527 40.632485,43.292687 0 0 1 1.00198,0.43379 40.632485,43.292687 0 0 1 0.99134,0.46232 40.632485,43.292687 0 0 1 0.97929,0.49017 40.632485,43.292687 0 0 1 0.96598,0.51804 40.632485,43.292687 0 0 1 0.9533,0.54456 40.632485,43.292687 0 0 1 0.93927,0.57176 40.632485,43.292687 0 0 1 0.92461,0.59829 40.632485,43.292687 0 0 1 0.90926,0.62482 40.632485,43.292687 0 0 1 0.89328,0.64937 40.632485,43.292687 0 0 1 0.87724,0.67523 40.632485,43.292687 0 0 1 0.85922,0.69977 40.632485,43.292687 0 0 1 0.8419,0.72366 40.632485,43.292687 0 0 1 0.82388,0.74753 40.632485,43.292687 0 0 1 0.80452,0.77008 40.632485,43.292687 0 0 1 0.78518,0.7933 40.632485,43.292687 0 0 1 0.7645,0.81453 40.632485,43.292687 0 0 1 0.74449,0.83641 40.632485,43.292687 0 0 1 0.72314,0.85764 40.632485,43.292687 0 0 1 0.70113,0.87754 40.632485,43.292687 0 0 1 0.67912,0.89677 40.632485,43.292687 0 0 1 0.65711,0.91601 40.632485,43.292687 0 0 1 0.63307,0.93391 40.632485,43.292687 0 0 1 0.6104,0.95183 40.632485,43.292687 0 0 1 0.58573,0.96907 40.632485,43.292687 0 0 1 0.56171,0.98499 40.632485,43.292687 0 0 1 0.53633,1.00091 40.632485,43.292687 0 0 1 0.51167,1.01551 40.632485,43.292687 0 0 1 0.48631,1.03009 40.632485,43.292687 0 0 1 0.45965,1.0427 40.632485,43.292687 0 0 1 0.43428,1.05596 40.632485,43.292687 0 0 1 0.40694,1.0679 40.632485,43.292687 0 0 1 0.38025,1.07852 40.632485,43.292687 0 0 1 0.35357,1.08979 40.632485,43.292687 0 0 1 0.32622,1.09908 40.632485,43.292687 0 0 1 0.29818,1.1077 40.632485,43.292687 0 0 1 0.27085,1.11566 40.632485,43.292687 0 0 1 0.24283,1.12362 40.632485,43.292687 0 0 1 0.2148,1.12959 40.632485,43.292687 0 0 1 0.18612,1.13489 40.632485,43.292687 0 0 1 0.15745,1.14021 40.632485,43.292687 0 0 1 0.12941,1.14484 40.632485,43.292687 0 0 1 0.10078,1.1475 40.632485,43.292687 0 0 1 0.072,1.15015 40.632485,43.292687 0 0 1 0.0434,1.15148 40.632485,43.292687 0 0 1 0.014,1.15214 40.632485,43.292687 0 0 1 -0.12477,3.39672 40.632485,43.292687 0 0 1 -0.37557,3.37617 40.632485,43.292687 0 0 1 -0.6224,3.33371 40.632485,43.292687 0 0 1 -0.86591,3.27203 40.632485,43.292687 0 0 1 -1.10406,3.18911 40.632485,43.292687 0 0 1 -1.33555,3.08697 40.632485,43.292687 0 0 1 -1.55902,2.96559 40.632485,43.292687 0 0 1 -1.77249,2.82696 40.632485,43.292687 0 0 1 -1.9753,2.66909 40.632485,43.292687 0 0 1 -2.16544,2.49664 40.632485,43.292687 0 0 1 -2.34287,2.30693 40.632485,43.292687 0 0 1 -2.50564,2.10463 40.632485,43.292687 0 0 1 -2.65309,1.88906 40.632485,43.292687 0 0 1 -2.78317,1.66089 40.632485,43.292687 0 0 1 -2.89791,1.42276 40.632485,43.292687 0 0 1 -2.99264,1.17668 40.632485,43.292687 0 0 1 -3.07067,0.92265 40.632485,43.292687 0 0 1 -3.12941,0.66329 40.632485,43.292687 0 0 1 -3.168074,0.3993 40.632485,43.292687 0 0 1 -3.188104,0.13399 40.632485,43.292687 0 0 1 -3.188094,-0.13399 40.632485,43.292687 0 0 1 -3.168088,-0.3993 40.632485,43.292687 0 0 1 -3.12939,-0.6633 40.632485,43.292687 0 0 1 -3.07069,-0.92264 40.632485,43.292687 0 0 1 -2.993303,-1.17668 40.632485,43.292687 0 0 1 -2.897244,-1.42277 40.632485,43.292687 0 0 1 -2.783837,-1.66088 40.632485,43.292687 0 0 1 -2.652404,-1.88906 40.632485,43.292687 0 0 1 -2.505648,-2.10464 40.632485,43.292687 0 0 1 -2.342886,-2.30693 40.632485,43.292687 0 0 1 -2.166082,-2.49664 40.632485,43.292687 0 0 1 -1.974639,-2.66909 40.632485,43.292687 0 0 1 -1.772504,-2.82696 40.632485,43.292687 0 0 1 -1.559022,-2.96558 40.632485,43.292687 0 0 1 -1.335549,-3.08697 40.632485,43.292687 0 0 1 -1.104725,-3.18912 40.632485,43.292687 0 0 1 -0.865908,-3.27202 40.632485,43.292687 0 0 1 -0.622408,-3.33372 40.632485,43.292687 0 0 1 -0.374918,-3.37616 40.632485,43.292687 0 0 1 -0.125409,-3.39673 40.632485,43.292687 0 0 1 0.125409,-3.39672 40.632485,43.292687 0 0 1 0.374918,-3.37551 40.632485,43.292687 0 0 1 0.622408,-3.33371 40.632485,43.292687 0 0 1 0.865908,-3.27203 40.632485,43.292687 0 0 1 1.104725,-3.18911 40.632485,43.292687 0 0 1 1.335549,-3.08697 40.632485,43.292687 0 0 1 1.559022,-2.96625 40.632485,43.292687 0 0 1 1.772504,-2.82629 40.632485,43.292687 0 0 1 1.974639,-2.66976 40.632485,43.292687 0 0 1 2.166082,-2.49597 40.632485,43.292687 0 0 1 2.342886,-2.3076 40.632485,43.292687 0 0 1 2.505648,-2.10463 40.632485,43.292687 0 0 1 2.652426,-1.8884 40.632485,43.292687 0 0 1 2.783836,-1.66089 40.632485,43.292687 0 0 1 2.897245,-1.42342 40.632485,43.292687 0 0 1 2.993303,-1.17602 40.632485,43.292687 0 0 1 3.070689,-0.92331 40.632485,43.292687 0 0 1 3.12939,-0.66263 40.632485,43.292687 0 0 1 3.16809,-0.39997 40.632485,43.292687 0 0 1 3.188093,-0.13332 z"
+ clip-path="url(#clipPath20679)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-5-7"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0376767;stroke-linejoin:round"
+ d="m 74.301703,221.87113 a 40.722405,43.678338 0 0 0 -40.72207,43.67871 40.722405,43.678338 0 0 0 40.72207,43.67813 40.722405,43.678338 0 0 0 40.722637,-43.67813 40.722405,43.678338 0 0 0 -40.722637,-43.67871 z m -0.0809,5.55645 a 35.68863,38.243712 0 0 1 0.94981,0.0135 35.68863,38.243712 0 0 1 0.94922,0.041 35.68863,38.243712 0 0 1 0.94805,0.0674 35.68863,38.243712 0 0 1 0.94628,0.0949 35.68863,38.243712 0 0 1 0.94336,0.12187 35.68863,38.243712 0 0 1 0.93984,0.14824 35.68863,38.243712 0 0 1 0.93574,0.1752 35.68863,38.243712 0 0 1 0.93165,0.20215 35.68863,38.243712 0 0 1 0.92578,0.22851 35.68863,38.243712 0 0 1 0.91992,0.25489 35.68863,38.243712 0 0 1 0.91348,0.28066 35.68863,38.243712 0 0 1 0.90586,0.30703 35.68863,38.243712 0 0 1 0.89765,0.33281 35.68863,38.243712 0 0 1 0.88945,0.35801 35.68863,38.243712 0 0 1 0.880067,0.3832 35.68863,38.243712 0 0 1 0.87071,0.4084 35.68863,38.243712 0 0 1 0.86015,0.43301 35.68863,38.243712 0 0 1 0.84844,0.45762 35.68863,38.243712 0 0 1 0.8373,0.48105 35.68863,38.243712 0 0 1 0.825,0.50508 35.68863,38.243712 0 0 1 0.81211,0.52852 35.68863,38.243712 0 0 1 0.79863,0.55195 35.68863,38.243712 0 0 1 0.78458,0.57363 35.68863,38.243712 0 0 1 0.7705,0.59649 35.68863,38.243712 0 0 1 0.75469,0.61816 35.68863,38.243712 0 0 1 0.73946,0.63926 35.68863,38.243712 0 0 1 0.72363,0.66035 35.68863,38.243712 0 0 1 0.70664,0.68027 35.68863,38.243712 0 0 1 0.68965,0.70078 35.68863,38.243712 0 0 1 0.67148,0.71954 35.68863,38.243712 0 0 1 0.65391,0.73886 35.68863,38.243712 0 0 1 0.63515,0.75762 35.68863,38.243712 0 0 1 0.61582,0.7752 35.68863,38.243712 0 0 1 0.59649,0.79218 35.68863,38.243712 0 0 1 0.57715,0.80918 35.68863,38.243712 0 0 1 0.55605,0.825 35.68863,38.243712 0 0 1 0.53613,0.84082 35.68863,38.243712 0 0 1 0.51446,0.85606 35.68863,38.243712 0 0 1 0.49336,0.87012 35.68863,38.243712 0 0 1 0.47109,0.88417 35.68863,38.243712 0 0 1 0.44941,0.89708 35.68863,38.243712 0 0 1 0.42715,0.90996 35.68863,38.243712 0 0 1 0.40371,0.92109 35.68863,38.243712 0 0 1 0.38145,0.93281 35.68863,38.243712 0 0 1 0.35742,0.94336 35.68863,38.243712 0 0 1 0.33399,0.95274 35.68863,38.243712 0 0 1 0.31054,0.96269 35.68863,38.243712 0 0 1 0.28653,0.9709 35.68863,38.243712 0 0 1 0.26191,0.97852 35.68863,38.243712 0 0 1 0.23789,0.98554 35.68863,38.243712 0 0 1 0.21328,0.99258 35.68863,38.243712 0 0 1 0.18867,0.99785 35.68863,38.243712 0 0 1 0.16348,1.00254 35.68863,38.243712 0 0 1 0.13828,1.00723 35.68863,38.243712 0 0 1 0.11367,1.01133 35.68863,38.243712 0 0 1 0.0885,1.01367 35.68863,38.243712 0 0 1 0.0633,1.01601 35.68863,38.243712 0 0 1 0.0381,1.01719 35.68863,38.243712 0 0 1 0.0123,1.01778 35.68863,38.243712 0 0 1 -0.10957,3.00058 35.68863,38.243712 0 0 1 -0.32988,2.98242 35.68863,38.243712 0 0 1 -0.54668,2.94492 35.68863,38.243712 0 0 1 -0.76055,2.89044 35.68863,38.243712 0 0 1 -0.96972,2.81718 35.68863,38.243712 0 0 1 -1.17305,2.72696 35.68863,38.243712 0 0 1 -1.36934,2.61972 35.68863,38.243712 0 0 1 -1.55683,2.49727 35.68863,38.243712 0 0 1 -1.73496,2.35781 35.68863,38.243712 0 0 1 -1.90196,2.20547 35.68863,38.243712 0 0 1 -2.05781,2.03789 35.68863,38.243712 0 0 1 -2.20078,1.85918 35.68863,38.243712 0 0 1 -2.33027,1.66875 35.68863,38.243712 0 0 1 -2.44454,1.46718 35.68863,38.243712 0 0 1 -2.54531,1.25684 35.68863,38.243712 0 0 1 -2.628507,1.03945 35.68863,38.243712 0 0 1 -2.69706,0.81504 35.68863,38.243712 0 0 1 -2.74864,0.58594 35.68863,38.243712 0 0 1 -2.78261,0.35274 35.68863,38.243712 0 0 1 -2.8002,0.11836 35.68863,38.243712 0 0 1 -2.80019,-0.11836 35.68863,38.243712 0 0 1 -2.78262,-0.35274 35.68863,38.243712 0 0 1 -2.74863,-0.58594 35.68863,38.243712 0 0 1 -2.69707,-0.81504 35.68863,38.243712 0 0 1 -2.6291,-1.03945 35.68863,38.243712 0 0 1 -2.54473,-1.25684 35.68863,38.243712 0 0 1 -2.44512,-1.46718 35.68863,38.243712 0 0 1 -2.32968,-1.66875 35.68863,38.243712 0 0 1 -2.20078,-1.85918 35.68863,38.243712 0 0 1 -2.05782,-2.03789 35.68863,38.243712 0 0 1 -1.90253,-2.20547 35.68863,38.243712 0 0 1 -1.73438,-2.35781 35.68863,38.243712 0 0 1 -1.55684,-2.49727 35.68863,38.243712 0 0 1 -1.36933,-2.61972 35.68863,38.243712 0 0 1 -1.17305,-2.72696 35.68863,38.243712 0 0 1 -0.97031,-2.81718 35.68863,38.243712 0 0 1 -0.76055,-2.89044 35.68863,38.243712 0 0 1 -0.54668,-2.94492 35.68863,38.243712 0 0 1 -0.3293,-2.98242 35.68863,38.243712 0 0 1 -0.11015,-3.00058 35.68863,38.243712 0 0 1 0.11015,-3.00059 35.68863,38.243712 0 0 1 0.3293,-2.98184 35.68863,38.243712 0 0 1 0.54668,-2.94492 35.68863,38.243712 0 0 1 0.76055,-2.89043 35.68863,38.243712 0 0 1 0.97031,-2.81719 35.68863,38.243712 0 0 1 1.17305,-2.72695 35.68863,38.243712 0 0 1 1.36933,-2.62031 35.68863,38.243712 0 0 1 1.55684,-2.49668 35.68863,38.243712 0 0 1 1.73438,-2.3584 35.68863,38.243712 0 0 1 1.90253,-2.20488 35.68863,38.243712 0 0 1 2.05782,-2.03848 35.68863,38.243712 0 0 1 2.20078,-1.85918 35.68863,38.243712 0 0 1 2.32968,-1.66816 35.68863,38.243712 0 0 1 2.44512,-1.46719 35.68863,38.243712 0 0 1 2.54473,-1.25742 35.68863,38.243712 0 0 1 2.6291,-1.03887 35.68863,38.243712 0 0 1 2.69707,-0.81562 35.68863,38.243712 0 0 1 2.74863,-0.58536 35.68863,38.243712 0 0 1 2.78262,-0.35332 35.68863,38.243712 0 0 1 2.80019,-0.11777 z"
+ clip-path="url(#clipPath20683)"
+ transform="translate(-206.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path1306-7-8"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0427732;stroke-linejoin:round;stroke-opacity:1"
+ d="m 119.27202,216.33781 a 46.363577,49.444797 0 0 0 -46.363193,49.44522 46.363577,49.444797 0 0 0 46.363193,49.44456 46.363577,49.444797 0 0 0 46.36384,-49.44456 46.363577,49.444797 0 0 0 -46.36384,-49.44522 z m -0.0921,6.29001 a 40.632485,43.292687 0 0 1 1.08139,0.0153 40.632485,43.292687 0 0 1 1.08071,0.0464 40.632485,43.292687 0 0 1 1.07938,0.0763 40.632485,43.292687 0 0 1 1.07737,0.10745 40.632485,43.292687 0 0 1 1.07404,0.13797 40.632485,43.292687 0 0 1 1.07003,0.16781 40.632485,43.292687 0 0 1 1.06537,0.19832 40.632485,43.292687 0 0 1 1.06071,0.22884 40.632485,43.292687 0 0 1 1.05402,0.25868 40.632485,43.292687 0 0 1 1.04736,0.28854 40.632485,43.292687 0 0 1 1.04002,0.31771 40.632485,43.292687 0 0 1 1.03134,0.34757 40.632485,43.292687 0 0 1 1.02201,0.37675 40.632485,43.292687 0 0 1 1.01266,0.40527 40.632485,43.292687 0 0 1 1.00198,0.4338 40.632485,43.292687 0 0 1 0.99134,0.46231 40.632485,43.292687 0 0 1 0.97929,0.49018 40.632485,43.292687 0 0 1 0.96598,0.51803 40.632485,43.292687 0 0 1 0.9533,0.54456 40.632485,43.292687 0 0 1 0.93927,0.57176 40.632485,43.292687 0 0 1 0.92461,0.59829 40.632485,43.292687 0 0 1 0.90926,0.62483 40.632485,43.292687 0 0 1 0.89328,0.64936 40.632485,43.292687 0 0 1 0.87724,0.67523 40.632485,43.292687 0 0 1 0.85922,0.69978 40.632485,43.292687 0 0 1 0.8419,0.72365 40.632485,43.292687 0 0 1 0.82388,0.74753 40.632485,43.292687 0 0 1 0.80452,0.77009 40.632485,43.292687 0 0 1 0.78518,0.7933 40.632485,43.292687 0 0 1 0.7645,0.81452 40.632485,43.292687 0 0 1 0.74449,0.83641 40.632485,43.292687 0 0 1 0.72314,0.85764 40.632485,43.292687 0 0 1 0.70113,0.87754 40.632485,43.292687 0 0 1 0.67912,0.89677 40.632485,43.292687 0 0 1 0.65711,0.91601 40.632485,43.292687 0 0 1 0.63307,0.93392 40.632485,43.292687 0 0 1 0.6104,0.95183 40.632485,43.292687 0 0 1 0.58573,0.96907 40.632485,43.292687 0 0 1 0.56171,0.98499 40.632485,43.292687 0 0 1 0.53634,1.00091 40.632485,43.292687 0 0 1 0.51167,1.0155 40.632485,43.292687 0 0 1 0.48631,1.0301 40.632485,43.292687 0 0 1 0.45965,1.04269 40.632485,43.292687 0 0 1 0.43428,1.05597 40.632485,43.292687 0 0 1 0.40694,1.0679 40.632485,43.292687 0 0 1 0.38025,1.07851 40.632485,43.292687 0 0 1 0.35357,1.0898 40.632485,43.292687 0 0 1 0.32622,1.09908 40.632485,43.292687 0 0 1 0.29818,1.10769 40.632485,43.292687 0 0 1 0.27085,1.11566 40.632485,43.292687 0 0 1 0.24283,1.12362 40.632485,43.292687 0 0 1 0.2148,1.12959 40.632485,43.292687 0 0 1 0.18612,1.1349 40.632485,43.292687 0 0 1 0.15745,1.1402 40.632485,43.292687 0 0 1 0.12941,1.14484 40.632485,43.292687 0 0 1 0.10078,1.1475 40.632485,43.292687 0 0 1 0.072,1.15015 40.632485,43.292687 0 0 1 0.0434,1.15148 40.632485,43.292687 0 0 1 0.014,1.15214 40.632485,43.292687 0 0 1 -0.12477,3.39673 40.632485,43.292687 0 0 1 -0.37557,3.37616 40.632485,43.292687 0 0 1 -0.6224,3.33371 40.632485,43.292687 0 0 1 -0.86591,3.27203 40.632485,43.292687 0 0 1 -1.10406,3.18912 40.632485,43.292687 0 0 1 -1.33555,3.08697 40.632485,43.292687 0 0 1 -1.55903,2.96558 40.632485,43.292687 0 0 1 -1.77249,2.82696 40.632485,43.292687 0 0 1 -1.9753,2.66909 40.632485,43.292687 0 0 1 -2.16544,2.49664 40.632485,43.292687 0 0 1 -2.34287,2.30693 40.632485,43.292687 0 0 1 -2.50564,2.10464 40.632485,43.292687 0 0 1 -2.65309,1.88906 40.632485,43.292687 0 0 1 -2.78317,1.66088 40.632485,43.292687 0 0 1 -2.89791,1.42277 40.632485,43.292687 0 0 1 -2.99263,1.17668 40.632485,43.292687 0 0 1 -3.07068,0.92264 40.632485,43.292687 0 0 1 -3.1294,0.66329 40.632485,43.292687 0 0 1 -3.16808,0.39931 40.632485,43.292687 0 0 1 -3.1881,0.13398 40.632485,43.292687 0 0 1 -3.1881,-0.13398 40.632485,43.292687 0 0 1 -3.16808,-0.39931 40.632485,43.292687 0 0 1 -3.12939,-0.66329 40.632485,43.292687 0 0 1 -3.07069,-0.92264 40.632485,43.292687 0 0 1 -2.99331,-1.17668 40.632485,43.292687 0 0 1 -2.89724,-1.42277 40.632485,43.292687 0 0 1 -2.783838,-1.66088 40.632485,43.292687 0 0 1 -2.652404,-1.88906 40.632485,43.292687 0 0 1 -2.505648,-2.10464 40.632485,43.292687 0 0 1 -2.342886,-2.30693 40.632485,43.292687 0 0 1 -2.166082,-2.49664 40.632485,43.292687 0 0 1 -1.974639,-2.66909 40.632485,43.292687 0 0 1 -1.772504,-2.82696 40.632485,43.292687 0 0 1 -1.559022,-2.96558 40.632485,43.292687 0 0 1 -1.335549,-3.08697 40.632485,43.292687 0 0 1 -1.104725,-3.18912 40.632485,43.292687 0 0 1 -0.865908,-3.27203 40.632485,43.292687 0 0 1 -0.622408,-3.33371 40.632485,43.292687 0 0 1 -0.374918,-3.37616 40.632485,43.292687 0 0 1 -0.125409,-3.39673 40.632485,43.292687 0 0 1 0.125409,-3.39673 40.632485,43.292687 0 0 1 0.374918,-3.3755 40.632485,43.292687 0 0 1 0.622408,-3.33371 40.632485,43.292687 0 0 1 0.865908,-3.27203 40.632485,43.292687 0 0 1 1.104725,-3.18911 40.632485,43.292687 0 0 1 1.335549,-3.08697 40.632485,43.292687 0 0 1 1.559022,-2.96625 40.632485,43.292687 0 0 1 1.772504,-2.8263 40.632485,43.292687 0 0 1 1.974639,-2.66975 40.632485,43.292687 0 0 1 2.166082,-2.49597 40.632485,43.292687 0 0 1 2.342886,-2.3076 40.632485,43.292687 0 0 1 2.505648,-2.10463 40.632485,43.292687 0 0 1 2.652426,-1.8884 40.632485,43.292687 0 0 1 2.783836,-1.66089 40.632485,43.292687 0 0 1 2.89724,-1.42343 40.632485,43.292687 0 0 1 2.99331,-1.17602 40.632485,43.292687 0 0 1 3.07069,-0.9233 40.632485,43.292687 0 0 1 3.12939,-0.66263 40.632485,43.292687 0 0 1 3.16809,-0.39997 40.632485,43.292687 0 0 1 3.18809,-0.13332 z"
+ clip-path="url(#clipPath20667)"
+ transform="translate(-251.48906,-227.91266)"
+ inkscape:export-filename="C:\Users\a116178\Downloads\Taler\taler 512.png"
+ inkscape:export-xdpi="96.231026"
+ inkscape:export-ydpi="96.231026"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-48.png b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-48.png
new file mode 100644
index 000000000..f13a23c85
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-48.png
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-512.png b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-512.png
new file mode 100644
index 000000000..be312ef55
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/static/img/taler-logo-512.png
Binary files differ
diff --git a/packages/taler-wallet-webextension/src/pwa/stories.html b/packages/taler-wallet-webextension/src/pwa/stories.html
new file mode 100644
index 000000000..f18307669
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/stories.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Stories</title>
+ <link rel="stylesheet" type="text/css" href="stories.css" />
+ <link rel="stylesheet" type="text/css" href="/static/font/import.css" />
+ <script src="stories.js"></script>
+ </head>
+ <body>
+ <taler-stories id="container"></taler-stories>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/src/pwa/sw.js b/packages/taler-wallet-webextension/src/pwa/sw.js
new file mode 100644
index 000000000..2b2219578
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/sw.js
@@ -0,0 +1,6 @@
+console.log("sw: Service worker installed");
+
+self.addEventListener("fetch", (event) => {
+ // console.log("fetch event", event);
+ // event.respondWith(/* custom content goes here */);
+});
diff --git a/packages/taler-wallet-webextension/src/pwa/tests.html b/packages/taler-wallet-webextension/src/pwa/tests.html
new file mode 100644
index 000000000..383f13d03
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/tests.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Mocha Tests</title>
+ <link rel="stylesheet" href="/mocha.css" />
+ </head>
+ <body>
+ <div id="mocha"></div>
+ <script src="/mocha.js"></script>
+ <script>
+ mocha.setup("bdd");
+ </script>
+
+ <!-- load code you want to test here -->
+ <script src="stories.test.js"></script>
+ <script src="hooks/useTalerActionURL.test.js"></script>
+ <!-- load your test files here -->
+
+ <script>
+ mocha.run();
+ </script>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/src/pwa/wallet.html b/packages/taler-wallet-webextension/src/pwa/wallet.html
new file mode 100644
index 000000000..366615dff
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pwa/wallet.html
@@ -0,0 +1,29 @@
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" type="text/css" href="walletEntryPoint.dev.css" />
+ <style>
+ html {
+ font-family: sans-serif; /* 1 */
+ }
+ h1 {
+ font-size: 2em;
+ }
+ input {
+ font: inherit;
+ }
+ body {
+ margin: 0;
+ font-size: 100%;
+ padding: 0;
+ background-color: #f8faf7;
+ font-family: Arial, Helvetica, sans-serif;
+ }
+ </style>
+ <script type="module" src="walletEntryPoint.dev.js"></script>
+ </head>
+
+ <body>
+ <div id="container" class="wallet-container"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/src/renderHtml.tsx b/packages/taler-wallet-webextension/src/renderHtml.tsx
deleted file mode 100644
index bbe8e465c..000000000
--- a/packages/taler-wallet-webextension/src/renderHtml.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 INRIA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Helpers functions to render Taler-related data structures to HTML.
- *
- * @author Florian Dold
- */
-
-/**
- * Imports.
- */
-import {
- AmountJson,
- Amounts,
- amountFractionalBase,
-} from "@gnu-taler/taler-util";
-import { Component, ComponentChildren, JSX, h } from "preact";
-
-/**
- * Render amount as HTML, which non-breaking space between
- * decimal value and currency.
- */
-export function renderAmount(amount: AmountJson | string): JSX.Element {
- let a;
- if (typeof amount === "string") {
- a = Amounts.parse(amount);
- } else {
- a = amount;
- }
- if (!a) {
- return <span>(invalid amount)</span>;
- }
- const x = a.value + a.fraction / amountFractionalBase;
- return (
- <span>
- {x}&nbsp;{a.currency}
- </span>
- );
-}
-
-export const AmountView = ({
- amount,
-}: {
- amount: AmountJson | string;
-}): JSX.Element => renderAmount(amount);
-
-/**
- * Abbreviate a string to a given length, and show the full
- * string on hover as a tooltip.
- */
-export function abbrev(s: string, n = 5): JSX.Element {
- let sAbbrev = s;
- if (s.length > n) {
- sAbbrev = s.slice(0, n) + "..";
- }
- return (
- <span class="abbrev" title={s}>
- {sAbbrev}
- </span>
- );
-}
-
-interface CollapsibleState {
- collapsed: boolean;
-}
-
-interface CollapsibleProps {
- initiallyCollapsed: boolean;
- title: string;
-}
-
-/**
- * Component that shows/hides its children when clicking
- * a heading.
- */
-export class Collapsible extends Component<
- CollapsibleProps,
- CollapsibleState
-> {
- constructor(props: CollapsibleProps) {
- super(props);
- this.state = { collapsed: props.initiallyCollapsed };
- }
- render(): JSX.Element {
- const doOpen = (e: any): void => {
- this.setState({ collapsed: false });
- e.preventDefault();
- };
- const doClose = (e: any): void => {
- this.setState({ collapsed: true });
- e.preventDefault();
- };
- if (this.state.collapsed) {
- return (
- <h2>
- <a class="opener opener-collapsed" href="#" onClick={doOpen}>
- {" "}
- {this.props.title}
- </a>
- </h2>
- );
- }
- return (
- <div>
- <h2>
- <a class="opener opener-open" href="#" onClick={doClose}>
- {" "}
- {this.props.title}
- </a>
- </h2>
- {this.props.children}
- </div>
- );
- }
-}
-
-interface ExpanderTextProps {
- text: string;
-}
-
-/**
- * Show a heading with a toggle to show/hide the expandable content.
- */
-export function ExpanderText({ text }: ExpanderTextProps): JSX.Element {
- return <span>{text}</span>;
-}
-
-export interface LoadingButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
- isLoading: boolean;
-}
-
-export function ProgressButton({isLoading, ...rest}: LoadingButtonProps): JSX.Element {
- return (
- <button
- class="pure-button pure-button-primary"
- type="button"
- {...rest}
- >
- {isLoading ? (
- <span>
- <object
- class="svg-icon svg-baseline"
- data="/img/spinner-bars.svg"
- />
- </span>
- ) : null}{" "}
- {rest.children}
- </button>
- );
-}
-
-export function PageLink(
- props: { pageName: string, children?: ComponentChildren },
-): JSX.Element {
- const url = chrome.extension.getURL(`/static/wallet.html#/${props.pageName}`);
- return (
- <a
- class="actionLink"
- href={url}
- target="_blank"
- rel="noopener noreferrer"
- >
- {props.children}
- </a>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/stories.test.ts b/packages/taler-wallet-webextension/src/stories.test.ts
new file mode 100644
index 000000000..b4af1bc1a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/stories.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 { setupI18n } from "@gnu-taler/taler-util";
+import { parseGroupImport } from "@gnu-taler/web-util/browser";
+import * as tests from "@gnu-taler/web-util/testing";
+import chromeAPI from "./platform/chrome.js";
+import { setupPlatform } from "./platform/foreground.js";
+
+import * as components from "./components/index.stories.js";
+import * as cta from "./cta/index.stories.js";
+import * as mui from "./mui/index.stories.js";
+import * as popup from "./popup/index.stories.js";
+import * as wallet from "./wallet/index.stories.js";
+// import { renderNodeOrBrowser } from "./test-utils.js";
+import { h, VNode, ComponentChildren } from "preact";
+import { AlertProvider } from "./context/alert.js";
+
+setupI18n("en", { en: {} });
+setupPlatform(chromeAPI);
+
+describe("All the examples:", () => {
+ const cms = parseGroupImport({ popup, wallet, cta, mui, components });
+ cms.forEach((group) => {
+ describe(`Example for group "${group.title}:"`, () => {
+ group.list.forEach((component) => {
+ describe(`Component ${component.name}:`, () => {
+ component.examples.forEach((example) => {
+ it(`should render example: ${example.name}`, () => {
+ tests.renderUI(example.render, DefaultTestingContext);
+ });
+ });
+ });
+ });
+ });
+ });
+});
+function DefaultTestingContext({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode {
+ //FIXME:
+ //some components push the alter in the UI function
+ //that's not correct, should be moved into the state function
+ // until then, we ran the tests with the alert provider
+ return h(AlertProvider, { children });
+}
diff --git a/packages/taler-wallet-webextension/src/stories.tsx b/packages/taler-wallet-webextension/src/stories.tsx
new file mode 100644
index 000000000..98ab301a3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/stories.tsx
@@ -0,0 +1,87 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { Fragment, FunctionComponent, h } from "preact";
+import { LogoHeader } from "./components/LogoHeader.js";
+import {
+ PopupBox,
+ WalletAction,
+ WalletBox,
+} from "./components/styled/index.js";
+import { strings } from "./i18n/strings.js";
+import { PopupNavBar, WalletNavBar } from "./NavigationBar.js";
+
+import * as components from "./components/index.stories.js";
+import * as cta from "./cta/index.stories.js";
+import * as mui from "./mui/index.stories.js";
+import * as popup from "./popup/index.stories.js";
+import * as wallet from "./wallet/index.stories.js";
+
+import { renderStories } from "@gnu-taler/web-util/browser";
+import { AlertProvider } from "./context/alert.js";
+
+function main(): void {
+ renderStories(
+ { popup, wallet, cta, mui, components },
+ {
+ strings,
+ getWrapperForGroup,
+ },
+ );
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
+function getWrapperForGroup(group: string): FunctionComponent {
+ switch (group) {
+ case "popup":
+ return function PopupWrapper({ children }: any) {
+ return (
+ <AlertProvider>
+ <PopupNavBar />
+ <PopupBox>{children}</PopupBox>
+ </AlertProvider>
+ );
+ };
+ case "wallet":
+ return function WalletWrapper({ children }: any) {
+ return (
+ <AlertProvider>
+ <LogoHeader />
+ <WalletNavBar />
+ <WalletBox>{children}</WalletBox>
+ </AlertProvider>
+ );
+ };
+ case "cta":
+ return function WalletWrapper({ children }: any) {
+ return (
+ <AlertProvider>
+ <WalletAction>{children}</WalletAction>
+ </AlertProvider>
+ );
+ };
+ default:
+ return Fragment;
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/svg/check_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/check_24px.inline.svg
new file mode 100644
index 000000000..522695ef3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/check_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/chevron-down.inline.svg b/packages/taler-wallet-webextension/src/svg/chevron-down.inline.svg
new file mode 100644
index 000000000..0e1c0aeda
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/chevron-down.inline.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 92 92" enable-background="new 0 0 92 92" xml:space="preserve">
+ <path id="XMLID_467_" d="M46,63c-1.1,0-2.1-0.4-2.9-1.2l-25-26c-1.5-1.6-1.5-4.1,0.1-5.7c1.6-1.5,4.1-1.5,5.7,0.1l22.1,23l22.1-23
+ c1.5-1.6,4.1-1.6,5.7-0.1c1.6,1.5,1.6,4.1,0.1,5.7l-25,26C48.1,62.6,47.1,63,46,63z" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/close_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/close_24px.inline.svg
new file mode 100644
index 000000000..8b7eb6e84
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/close_24px.inline.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <path d="M0 0h24v24H0z" fill="none" />
+ <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/delete_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/delete_24px.inline.svg
new file mode 100644
index 000000000..ead7caa9f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/delete_24px.inline.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+ <path d="M0 0h24v24H0z" fill="none" />
+ <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/download_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/download_24px.inline.svg
new file mode 100644
index 000000000..c4ec1c354
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/download_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z"/></g></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/edit_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/edit_24px.inline.svg
new file mode 100644
index 000000000..a4b3c9f6b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/edit_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/error_outline_outlined_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/error_outline_outlined_24px.inline.svg
new file mode 100644
index 000000000..4a0daa1e9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/error_outline_outlined_24px.inline.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <path d="M11 15h2v2h-2v-2zm0-8h2v6h-2V7zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/index.tsx b/packages/taler-wallet-webextension/src/svg/index.tsx
new file mode 100644
index 000000000..7de1c7d4f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/index.tsx
@@ -0,0 +1,38 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { h, VNode } from "preact";
+
+export const CopyIcon = (): VNode => (
+ <svg height="16" viewBox="0 0 16 16" width="16">
+ <path
+ fill-rule="evenodd"
+ d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
+ />
+ <path
+ fill-rule="evenodd"
+ d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
+ />
+ </svg>
+);
+
+export const CopiedIcon = (): VNode => (
+ <svg height="16" viewBox="0 0 16 16" width="16">
+ <path
+ fill-rule="evenodd"
+ d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
+ />
+ </svg>
+);
diff --git a/packages/taler-wallet-webextension/src/svg/info_outlined_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/info_outlined_24px.inline.svg
new file mode 100644
index 000000000..80dad95cc
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/info_outlined_24px.inline.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <path d="M0 0h24v24H0V0z" fill="none" />
+ <path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/logo-2021.inline.svg b/packages/taler-wallet-webextension/src/svg/logo-2021.inline.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/logo-2021.inline.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/progress.inline.svg b/packages/taler-wallet-webextension/src/svg/progress.inline.svg
new file mode 100644
index 000000000..c7284a545
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/progress.inline.svg
@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin:auto;background:#fff;display:block;" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
+ <defs>
+ <clipPath id="progress-cp" x="0" y="0" width="100" height="100">
+ <rect x="0" y="0" width="0" height="100">
+ <animate attributeName="width" repeatCount="indefinite" dur="2s" values="0;100;100" keyTimes="0;0.5;1"></animate>
+ <animate attributeName="x" repeatCount="indefinite" dur="2s" values="0;0;100" keyTimes="0;0.5;1"></animate>
+ </rect>
+ </clipPath>
+ </defs>
+ <path fill="none" stroke="darkgrey" stroke-width="1.04" d="M10.000000000000004 44.019999999999996L89.99999999999999 44.019999999999996A5.98 5.98 0 0 1 95.97999999999999 50L95.97999999999999 50A5.98 5.98 0 0 1 89.99999999999999 55.980000000000004L10.000000000000004 55.980000000000004A5.98 5.98 0 0 1 4.020000000000003 50L4.020000000000003 50A5.98 5.98 0 0 1 10.000000000000004 44.019999999999996 Z"></path>
+ <path fill="#0042b2" clip-path="url(#progress-cp)" d="M10.000000000000004 45.54L90 45.54A4.460000000000001 4.460000000000001 0 0 1 94.46 50L94.46 50A4.460000000000001 4.460000000000001 0 0 1 90 54.46L10.000000000000004 54.46A4.460000000000001 4.460000000000001 0 0 1 5.540000000000003 50L5.540000000000003 50A4.460000000000001 4.460000000000001 0 0 1 10.000000000000004 45.54 Z"></path>
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/qr_code_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/qr_code_24px.inline.svg
new file mode 100644
index 000000000..c0c158359
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/qr_code_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M3,11h8V3H3V11z M5,5h4v4H5V5z"/><path d="M3,21h8v-8H3V21z M5,15h4v4H5V15z"/><path d="M13,3v8h8V3H13z M19,9h-4V5h4V9z"/><rect height="2" width="2" x="19" y="19"/><rect height="2" width="2" x="13" y="13"/><rect height="2" width="2" x="15" y="15"/><rect height="2" width="2" x="13" y="17"/><rect height="2" width="2" x="15" y="19"/><rect height="2" width="2" x="17" y="17"/><rect height="2" width="2" x="17" y="13"/><rect height="2" width="2" x="19" y="15"/></g></g></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/refresh_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/refresh_24px.inline.svg
new file mode 100644
index 000000000..b8d69f402
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/refresh_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/refresh_outlined_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/refresh_outlined_24px.inline.svg
new file mode 100644
index 000000000..1959c1aad
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/refresh_outlined_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/refresh_rounded_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/refresh_rounded_24px.inline.svg
new file mode 100644
index 000000000..46db14360
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/refresh_rounded_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.65 6.35c-1.63-1.63-3.94-2.57-6.48-2.31-3.67.37-6.69 3.35-7.1 7.02C3.52 15.91 7.27 20 12 20c3.19 0 5.93-1.87 7.21-4.56.32-.67-.16-1.44-.9-1.44-.37 0-.72.2-.88.53-1.13 2.43-3.84 3.97-6.8 3.31-2.22-.49-4.01-2.3-4.48-4.52C5.31 9.44 8.26 6 12 6c1.66 0 3.14.69 4.22 1.78l-1.51 1.51c-.63.63-.19 1.71.7 1.71H19c.55 0 1-.45 1-1V6.41c0-.89-1.08-1.34-1.71-.71l-.64.65z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/refresh_sharp_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/refresh_sharp_24px.inline.svg
new file mode 100644
index 000000000..1959c1aad
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/refresh_sharp_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/refresh_two_tone_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/refresh_two_tone_24px.inline.svg
new file mode 100644
index 000000000..1959c1aad
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/refresh_two_tone_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/report_problem_outlined_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/report_problem_outlined_24px.inline.svg
new file mode 100644
index 000000000..ed32f6f28
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/report_problem_outlined_24px.inline.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <path d="M0 0h24v24H0V0z" fill="none" />
+ <path d="M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/ri-bank-line.inline.svg b/packages/taler-wallet-webextension/src/svg/ri-bank-line.inline.svg
new file mode 100644
index 000000000..8d987df79
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/ri-bank-line.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2 20h20v2H2v-2zm2-8h2v7H4v-7zm5 0h2v7H9v-7zm4 0h2v7h-2v-7zm5 0h2v7h-2v-7zM2 7l10-5 10 5v4H2V7zm2 1.236V9h16v-.764l-8-4-8 4zM12 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg>
diff --git a/packages/taler-wallet-webextension/src/svg/ri-file-unknown-line.svg b/packages/taler-wallet-webextension/src/svg/ri-file-unknown-line.svg
new file mode 100644
index 000000000..5203d49f5
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/ri-file-unknown-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 15h2v2h-2v-2zm2-1.645V14h-2v-1.5a1 1 0 0 1 1-1 1.5 1.5 0 1 0-1.471-1.794l-1.962-.393A3.501 3.501 0 1 1 13 13.355zM15 4H5v16h14V8h-4V4zM3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008V2.992z"/></svg>
diff --git a/packages/taler-wallet-webextension/src/svg/ri-hand-heart-line.svg b/packages/taler-wallet-webextension/src/svg/ri-hand-heart-line.svg
new file mode 100644
index 000000000..a9c195eac
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/ri-hand-heart-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 9a1 1 0 0 1 1 1 6.97 6.97 0 0 1 4.33 1.5h2.17c1.332 0 2.53.579 3.353 1.499L19 13a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.79 0-5.15-.603-7.06-1.658A.998.998 0 0 1 5 20H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h3zm1.001 3L6 17.021l.045.033C7.84 18.314 10.178 19 13 19c3.004 0 5.799-1.156 7.835-3.13l.133-.133-.12-.1a2.994 2.994 0 0 0-1.643-.63L19 15l-2.112-.001c.073.322.112.657.112 1.001v1H8v-2l6.79-.001-.034-.078a2.501 2.501 0 0 0-2.092-1.416L12.5 13.5H9.57A4.985 4.985 0 0 0 6.002 12zM4 11H3v7h1v-7zm9.646-7.425L14 3.93l.354-.354a2.5 2.5 0 1 1 3.535 3.536L14 11l-3.89-3.89a2.5 2.5 0 1 1 3.536-3.535zm-2.12 1.415a.5.5 0 0 0-.06.637l.058.069L14 8.17l2.476-2.474a.5.5 0 0 0 .058-.638l-.058-.07a.5.5 0 0 0-.638-.057l-.07.058-1.769 1.768-1.767-1.77-.068-.056a.5.5 0 0 0-.638.058z"/></svg>
diff --git a/packages/taler-wallet-webextension/src/svg/ri-refresh-line.svg b/packages/taler-wallet-webextension/src/svg/ri-refresh-line.svg
new file mode 100644
index 000000000..6efa8554b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/ri-refresh-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5.463 4.433A9.961 9.961 0 0 1 12 2c5.523 0 10 4.477 10 10 0 2.136-.67 4.116-1.81 5.74L17 12h3A8 8 0 0 0 6.46 6.228l-.997-1.795zm13.074 15.134A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12c0-2.136.67-4.116 1.81-5.74L7 12H4a8 8 0 0 0 13.54 5.772l.997 1.795z"/></svg>
diff --git a/packages/taler-wallet-webextension/src/svg/ri-refund-2-line.svg b/packages/taler-wallet-webextension/src/svg/ri-refund-2-line.svg
new file mode 100644
index 000000000..5805daf09
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/ri-refund-2-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5.671 4.257c3.928-3.219 9.733-2.995 13.4.672 3.905 3.905 3.905 10.237 0 14.142-3.905 3.905-10.237 3.905-14.142 0A9.993 9.993 0 0 1 2.25 9.767l.077-.313 1.934.51a8 8 0 1 0 3.053-4.45l-.221.166 1.017 1.017-4.596 1.06 1.06-4.596 1.096 1.096zM13 6v2h2.5v2H10a.5.5 0 0 0-.09.992L10 11h4a2.5 2.5 0 1 1 0 5h-1v2h-2v-2H8.5v-2H14a.5.5 0 0 0 .09-.992L14 13h-4a2.5 2.5 0 1 1 0-5h1V6h2z"/></svg>
diff --git a/packages/taler-wallet-webextension/src/svg/ri-shopping-cart-line.svg b/packages/taler-wallet-webextension/src/svg/ri-shopping-cart-line.svg
new file mode 100644
index 000000000..50dabf446
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/ri-shopping-cart-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M4 16V4H2V2h3a1 1 0 0 1 1 1v12h12.438l2-8H8V5h13.72a1 1 0 0 1 .97 1.243l-2.5 10a1 1 0 0 1-.97.757H5a1 1 0 0 1-1-1zm2 7a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm12 0a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></svg>
diff --git a/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg
new file mode 100644
index 000000000..d880cbf0f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/search_24px.inline.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" height="24" width="24">
+ <path fill-rule="evenodd" d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z" clip-rule="evenodd" />
+</svg>
+
diff --git a/packages/taler-wallet-webextension/src/svg/send_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/send_24px.inline.svg
new file mode 100644
index 000000000..6e41d1c9e
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/send_24px.inline.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+ <path d="M0 0h24v24H0z" fill="none" />
+ <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/settings_black_24dp.inline.svg b/packages/taler-wallet-webextension/src/svg/settings_black_24dp.inline.svg
new file mode 100644
index 000000000..adcd50405
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/settings_black_24dp.inline.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
+ <g>
+ <path d="M0,0h24v24H0V0z" fill="none" />
+ <path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z" />
+ </g>
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/spinner-bars.svg b/packages/taler-wallet-webextension/src/svg/spinner-bars.svg
new file mode 100644
index 000000000..f6f7dfcb3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/spinner-bars.svg
@@ -0,0 +1,53 @@
+<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
+<svg width="135" height="140" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#fff">
+ <rect y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.5s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.5s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="30" y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.25s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.25s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="60" width="15" height="140" rx="6">
+ <animate attributeName="height"
+ begin="0s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="90" y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.25s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.25s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="120" y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.5s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.5s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+</svg>
diff --git a/packages/taler-wallet-webextension/src/svg/success_outlined_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/success_outlined_24px.inline.svg
new file mode 100644
index 000000000..c6130b495
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/success_outlined_24px.inline.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
+ <path d="M0 0h24v24H0V0z" fill="none" />
+ <path d="M20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4C12.76,4 13.5,4.11 14.2, 4.31L15.77,2.74C14.61,2.26 13.34,2 12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0, 0 22,12M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z" />
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/taler-logo-2021-plain.svg b/packages/taler-wallet-webextension/src/svg/taler-logo-2021-plain.svg
new file mode 100644
index 000000000..6e3cc254f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/taler-logo-2021-plain.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ width="670"
+ height="300"
+ viewBox="0 0 201 90"
+ version="1.1"
+ id="svg8">
+ <g
+ id="logo">
+ <g
+ id="circles"
+ style="fill:#0042b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943">
+ <path
+ d="m 86.662153,1.1211936 c 15.589697,0 29.129227,9.4011664 35.961027,23.2018054 h -5.81736 C 110.4866,13.623304 99.349002,6.5180852 86.662153,6.5180852 c -19.690571,0 -35.652876,17.1120008 -35.652876,38.2205688 0,10.331797 3.825597,19.704678 10.03957,26.582945 -1.342357,1.120912 -2.771532,2.127905 -4.275488,3.006754 C 50.071485,66.553412 45.974857,56.15992 45.974857,44.738654 c 0,-24.089211 18.216325,-43.6174604 40.687296,-43.6174604 z M 122.51416,65.375898 c -6.86645,13.680134 -20.34561,22.980218 -35.852007,22.980218 -1.052702,0 -2.096093,-0.04291 -3.128683,-0.127026 3.052192,-1.561167 5.913582,-3.480387 8.538307,-5.707305 10.320963,-1.684389 19.185983,-8.113638 24.601813,-17.145887 z"
+ id="path2350" />
+ <path
+ d="m 64.212372,1.1211936 c 1.052607,0 2.095998,0.042919 3.128684,0.1270583 C 64.288864,2.8094199 61.427378,4.728606 58.802653,6.9555572 41.679542,9.7498571 28.559494,25.601563 28.559494,44.738654 c 0,14.264563 7.29059,26.702023 18.093843,33.268925 -1.593656,0.26719 -3.226966,0.406948 -4.890748,0.406948 -1.239545,0 -2.46151,-0.07952 -3.663522,-0.229364 C 29.191129,70.184015 23.525076,58.171633 23.525076,44.738654 23.525076,20.649443 41.7414,1.1211936 64.212372,1.1211936 Z M 69.62209,82.521785 C 79.943207,80.837396 88.808164,74.407841 94.224059,65.375422 h 5.840511 c -6.866354,13.680305 -20.345548,22.980694 -35.852198,22.980694 -1.052703,0 -2.095999,-0.04291 -3.128684,-0.127026 3.052002,-1.561371 5.913836,-3.480218 8.538402,-5.707305 z M 94.355885,24.322999 c -3.13939,-5.314721 -7.467551,-9.74275 -12.584511,-12.853269 1.593656,-0.26719 3.226904,-0.406948 4.890779,-0.406948 1.239451,0 2.461512,0.07952 3.663524,0.229364 4.016018,3.607242 7.373195,8.030111 9.849053,13.030853 z"
+ id="path2352" />
+ <path
+ d="m 41.762589,1.1211936 c 1.064296,0 2.118804,0.044379 3.162607,0.1302161 -3.046523,1.558961 -5.903162,3.4745139 -8.52358,5.6968133 C 19.254624,9.7205882 6.1097128,25.583465 6.1097128,44.738654 c 0,21.108568 15.9624012,38.22057 35.6528762,38.22057 12.599746,0 23.672446,-7.007056 30.013748,-17.583802 h 5.838515 C 70.748498,79.055727 57.26924,88.356116 41.762589,88.356116 c -22.470907,0 -40.6871998,-19.52825 -40.6871998,-43.617462 0,-24.089211 18.2162928,-43.6174604 40.6871998,-43.6174604 z M 71.905375,24.322999 c -1.31192,-2.220567 -2.830984,-4.287049 -4.528877,-6.166508 1.342452,-1.120945 2.771374,-2.128381 4.275139,-3.00723 2.372984,2.753011 4.418875,5.834636 6.072489,9.173738 z"
+ id="path2354" />
+ </g>
+ <g
+ id="letters">
+ <path
+ d="m 76.135411,34.409066 h 9.161042 V 29.36588 H 61.857537 v 5.043186 h 9.161137 v 25.92317 h 5.116737 z"
+ id="path2346" />
+ <path
+ d="m 92.647571,52.856334 h 13.659009 l 2.93009,7.476072 h 5.36461 L 101.89122,29.144903 H 97.187186 L 84.477089,60.332406 h 5.199533 z m 11.802109,-4.822276 h -9.944771 l 4.951718,-12.386462 z"
+ id="path2362" />
+ <path
+ d="m 123.80641,29.366084 h -4.58038 v 30.966322 h 20.54728 v -4.910253 c -5.32227,0 -10.64463,0 -15.9669,0 z"
+ id="path2356" />
+ <path
+ d="m 166.4722,29.366084 h -21.37564 v 30.966322 h 21.58203 v -4.910253 h -16.54771 v -8.27275 h 14.48439 V 42.23925 h -14.48439 v -7.962811 h 16.34132 z"
+ id="path2360" />
+ <path
+ d="m 191.19035,39.474593 c 0,1.59947 -0.53646,2.87535 -1.61628,3.818883 -1.07281,0.95124 -2.52409,1.422837 -4.34678,1.422837 h -7.44851 V 34.276439 h 7.4073 c 1.9051,0 3.38376,0.435027 4.42939,1.312178 1.05226,0.870258 1.57488,2.167734 1.57488,3.885976 z m 6.06602,20.857813 -7.79911,-11.723191 c 1.01771,-0.294794 1.94631,-0.714813 2.78553,-1.260566 0.83885,-0.545619 1.56122,-1.209263 2.16629,-1.990627 0.60541,-0.781738 1.07981,-1.681096 1.42369,-2.698345 0.34378,-1.017553 0.51561,-2.175238 0.51561,-3.472883 0,-1.50409 -0.24743,-2.867948 -0.74267,-4.092048 -0.49515,-1.223794 -1.20344,-2.256186 -2.12499,-3.096734 -0.92173,-0.840446 -2.04957,-1.489252 -3.38375,-1.946452 -1.33447,-0.457267 -2.82692,-0.685476 -4.4774,-0.685476 h -12.87512 v 30.966322 h 5.03433 V 49.538522 h 6.37569 l 7.11829,10.793884 z"
+ id="path2358" />
+ </g>
+ </g>
+</svg>
diff --git a/packages/taler-wallet-webextension/src/svg/upload_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/upload_24px.inline.svg
new file mode 100644
index 000000000..c604ef78d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/upload_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><path d="M5,20h14v-2H5V20z M5,10h4v6h6v-6h4l-7-7L5,10z"/></g></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/warning_24px.inline.svg b/packages/taler-wallet-webextension/src/svg/warning_24px.inline.svg
new file mode 100644
index 000000000..d27c4c6ec
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/warning_24px.inline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/svg/wifi.inline.svg b/packages/taler-wallet-webextension/src/svg/wifi.inline.svg
new file mode 100644
index 000000000..ad712435d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/svg/wifi.inline.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px">
+ <path d="M23.64 7c-.45-.34-4.93-4-11.64-4-1.5 0-2.89.19-4.15.48L18.18 13.8 23.64 7zm-6.6 8.22L3.27 1.44 2 2.72l2.05 2.06C1.91 5.76.59 6.82.36 7l11.63 14.49.01.01.01-.01 3.9-4.86 3.32 3.32 1.27-1.27-3.46-3.46z"></path>
+</svg> \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
new file mode 100644
index 000000000..3b7cbcbb7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-loader.ts
@@ -0,0 +1,372 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { CoreApiResponse, TalerError, TalerErrorCode } from "@gnu-taler/taler-util";
+import type { MessageFromBackend } from "./platform/api.js";
+
+/**
+ * This will modify all the pages that the user load when navigating with Web Extension enabled
+ *
+ * Can't do useful integration since it run in ISOLATED (or equivalent) mode.
+ *
+ * If taler support is expected, it will inject a script which will complete the integration.
+ */
+
+// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#content_script_environment
+
+// ISOLATED mode in chromium browsers
+// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#world
+// X-Ray vision in Firefox
+// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts#xray_vision_in_firefox
+
+// *** IMPORTANT ***
+
+// Content script lifecycle during navigation
+// In Firefox: Content scripts remain injected in a web page after the user has navigated away,
+// however, window object properties are destroyed.
+// In Chrome: Content scripts are destroyed when the user navigates away from a web page.
+
+const documentDocTypeIsHTML =
+ window.document.doctype && window.document.doctype.name === "html";
+const suffixIsNotXMLorPDF =
+ !window.location.pathname.endsWith(".xml") &&
+ !window.location.pathname.endsWith(".pdf");
+const rootElementIsHTML =
+ document.documentElement.nodeName &&
+ document.documentElement.nodeName.toLowerCase() === "html";
+// const pageAcceptsTalerSupport = document.head.querySelector(
+// "meta[name=taler-support]",
+// );
+
+
+
+function validateTalerUri(uri: string): boolean {
+ return (
+ !!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
+ );
+}
+
+function convertURIToWebExtensionPath(uri: string) {
+ const url = new URL(
+ chrome.runtime.getURL(`static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`),
+ );
+ return url.href;
+}
+
+// safe check, if one of this is true then taler handler is not useful
+// or not expected
+const shouldNotInject =
+ !documentDocTypeIsHTML ||
+ !suffixIsNotXMLorPDF ||
+ // !pageAcceptsTalerSupport ||
+ !rootElementIsHTML;
+
+const logger = {
+ debug: (...msg: any[]) => { },
+ info: (...msg: any[]) =>
+ console.log(`${new Date().toISOString()} TALER`, ...msg),
+ error: (...msg: any[]) =>
+ console.error(`${new Date().toISOString()} TALER`, ...msg),
+};
+
+// logger.debug = logger.info
+
+/**
+ */
+function redirectToTalerActionHandler(element: HTMLMetaElement) {
+ const name = element.getAttribute("name")
+ if (!name) return;
+ if (name !== "taler-uri") return;
+ const uri = element.getAttribute("content");
+ if (!uri) return;
+
+ if (!validateTalerUri(uri)) {
+ logger.error(`taler:// URI is invalid: ${uri}`);
+ return;
+ }
+
+ const walletPage = convertURIToWebExtensionPath(uri)
+ window.location.replace(walletPage)
+}
+
+function injectTalerSupportScript(head: HTMLHeadElement, trusted: boolean) {
+ const meta = head.querySelector("meta[name=taler-support]")
+ if (!meta) return;
+ const content = meta.getAttribute("content");
+ if (!content) return;
+ const features = content.split(",")
+
+ const debugEnabled = meta.getAttribute("debug") === "true";
+ const hijackEnabled = features.indexOf("uri") !== -1
+ const talerApiEnabled = features.indexOf("api") !== -1 && trusted
+
+ const scriptTag = document.createElement("script");
+ scriptTag.setAttribute("async", "false");
+ const url = new URL(
+ chrome.runtime.getURL("/dist/taler-wallet-interaction-support.js"),
+ );
+ url.searchParams.set("id", chrome.runtime.id);
+ if (debugEnabled) {
+ url.searchParams.set("debug", "true");
+ }
+ if (talerApiEnabled) {
+ url.searchParams.set("api", "true");
+ }
+ if (hijackEnabled) {
+ url.searchParams.set("hijack", "true");
+ }
+ scriptTag.src = url.href;
+
+ try {
+ head.insertBefore(scriptTag, head.children.length ? head.children[0] : null);
+ } catch (e) {
+ logger.info("inserting link handler failed!");
+ logger.error(e);
+ }
+}
+
+
+export interface ExtensionOperations {
+ isAutoOpenEnabled: {
+ request: void;
+ response: boolean;
+ };
+ isDomainTrusted: {
+ request: {
+ domain: string;
+ };
+ response: boolean;
+ };
+}
+
+export type MessageFromExtension<Op extends keyof ExtensionOperations> = {
+ channel: "extension";
+ operation: Op;
+ payload: ExtensionOperations[Op]["request"];
+};
+
+export type MessageResponse = CoreApiResponse;
+
+async function callBackground<Op extends keyof ExtensionOperations>(
+ operation: Op,
+ payload: ExtensionOperations[Op]["request"],
+): Promise<ExtensionOperations[Op]["response"]> {
+ const message: MessageFromExtension<Op> = {
+ channel: "extension",
+ operation,
+ payload,
+ };
+
+ const response = await sendMessageToBackground(message);
+ if (response.type === "error") {
+ throw new Error(`Background operation "${operation}" failed`);
+ }
+ return response.result as any;
+}
+
+
+let nextMessageIndex = 0;
+/**
+ *
+ * @param message
+ * @returns
+ */
+async function sendMessageToBackground<Op extends keyof ExtensionOperations>(
+ message: MessageFromExtension<Op>,
+): Promise<MessageResponse> {
+ const messageWithId = { ...message, id: `id_${nextMessageIndex++ % 1000}` };
+
+ if (!chrome.runtime.id) {
+ return Promise.reject(TalerError.fromDetail(TalerErrorCode.WALLET_CORE_NOT_AVAILABLE, {}))
+ }
+ return new Promise<any>((resolve, reject) => {
+ logger.debug("send operation to the wallet background", message, chrome.runtime.id);
+ let timedout = false;
+ const timerId = setTimeout(() => {
+ timedout = true;
+ reject(TalerError.fromDetail(TalerErrorCode.GENERIC_TIMEOUT, {
+ requestMethod: "wallet",
+ requestUrl: message.operation,
+ timeoutMs: 20 * 1000,
+ }))
+ }, 20 * 1000); //five seconds
+ try {
+ chrome.runtime.sendMessage(messageWithId, (backgroundResponse) => {
+ if (timedout) {
+ return false; //already rejected
+ }
+ clearTimeout(timerId);
+ if (chrome.runtime.lastError) {
+ reject(chrome.runtime.lastError.message);
+ } else {
+ resolve(backgroundResponse);
+ }
+ // return true to keep the channel open
+ return true;
+ });
+ } catch (e) {
+ console.log(e)
+ }
+ });
+}
+
+let notificationPort: chrome.runtime.Port | undefined;
+function listenToWalletBackground(listener: (m: any) => void): () => void {
+ if (notificationPort === undefined) {
+ notificationPort = chrome.runtime.connect({ name: "notifications" });
+ }
+ notificationPort.onMessage.addListener(listener);
+ function removeListener(): void {
+ if (notificationPort !== undefined) {
+ notificationPort.onMessage.removeListener(listener);
+ }
+ }
+ return removeListener;
+}
+
+const loaderSettings = {
+ isAutoOpenEnabled: false,
+ isDomainTrusted: false,
+}
+
+function start(
+ onTalerMetaTagFound: (listener: (el: HTMLMetaElement) => void) => void,
+ onHeadReady: (listener: (el: HTMLHeadElement) => void) => void
+) {
+ // do not run everywhere, this is just expected to run on site
+ // that are aware of taler
+ if (shouldNotInject) return;
+
+ const isAutoOpenEnabled_promise = callBackground("isAutoOpenEnabled", undefined).then(result => {
+ loaderSettings.isAutoOpenEnabled = result;
+ return result;
+ })
+ const isDomainTrusted_promise = callBackground("isDomainTrusted", {
+ domain: window.location.origin
+ }).then(result => {
+ loaderSettings.isDomainTrusted = result;
+ return result;
+ })
+
+ onTalerMetaTagFound(async (el) => {
+ await isAutoOpenEnabled_promise;
+ if (!loaderSettings.isAutoOpenEnabled) {
+ return;
+ }
+ redirectToTalerActionHandler(el)
+ })
+
+ onHeadReady(async (el) => {
+ const trusted = await isDomainTrusted_promise
+ injectTalerSupportScript(el, trusted)
+ })
+
+ listenToWalletBackground((e: MessageFromBackend) => {
+ if (e.type === "web-extension" && e.notification.type === "settings-change") {
+ const settings = e.notification.currentValue
+ loaderSettings.isAutoOpenEnabled = settings.autoOpen
+ }
+ })
+
+}
+
+function isCorrectMetaElement(el: HTMLMetaElement): boolean {
+ const name = el.getAttribute("name")
+ if (!name) return false;
+ if (name !== "taler-uri") return false;
+ const uri = el.getAttribute("content");
+ if (!uri) return false;
+ return true
+}
+
+/**
+ * Tries to find taler meta tag ASAP and report
+ * @param notify
+ * @returns
+ */
+function notifyWhenTalerUriIsFound(notify: (el: HTMLMetaElement) => void) {
+ if (document.head) {
+ const element = document.head.querySelector("meta[name=taler-uri]")
+ if (!element) return;
+ if (!(element instanceof HTMLMetaElement)) return;
+
+ if (isCorrectMetaElement(element)) {
+ notify(element)
+ }
+ return;
+ }
+ const obs = new MutationObserver(async function (mutations) {
+ try {
+ mutations.forEach((mut) => {
+ if (mut.type === "childList") {
+ mut.addedNodes.forEach((added) => {
+ if (added instanceof HTMLMetaElement) {
+ if (isCorrectMetaElement(added)) {
+ notify(added)
+ obs.disconnect()
+ }
+ }
+ });
+ }
+ });
+ } catch (e) {
+ console.error(e)
+ }
+ })
+
+ obs.observe(document, {
+ childList: true,
+ subtree: true,
+ attributes: false,
+ })
+
+}
+
+/**
+ * Tries to find HEAD tag ASAP and report
+ * @param notify
+ * @returns
+ */
+function notifyWhenHeadIsFound(notify: (el: HTMLHeadElement) => void) {
+ if (document.head) {
+ notify(document.head)
+ return;
+ }
+ const obs = new MutationObserver(async function (mutations) {
+ try {
+ mutations.forEach((mut) => {
+ if (mut.type === "childList") {
+ mut.addedNodes.forEach((added) => {
+ if (added instanceof HTMLHeadElement) {
+ notify(added)
+ obs.disconnect()
+ }
+ });
+ }
+ });
+ } catch (e) {
+ console.error(e)
+ }
+ })
+
+ obs.observe(document, {
+ childList: true,
+ subtree: true,
+ attributes: false,
+ })
+}
+
+start(notifyWhenTalerUriIsFound, notifyWhenHeadIsFound);
diff --git a/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
new file mode 100644
index 000000000..8b15380f9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/taler-wallet-interaction-support.ts
@@ -0,0 +1,200 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * WARNING
+ *
+ * This script will be loaded and run in every page while the
+ * user us navigating. It must be short, simple and safe.
+ */
+(() => {
+ const logger = {
+ debug: (...msg: any[]) => { },
+ info: (...msg: any[]) =>
+ console.log(`${new Date().toISOString()} TALER`, ...msg),
+ error: (...msg: any[]) =>
+ console.error(`${new Date().toISOString()} TALER`, ...msg),
+ };
+
+ const documentDocTypeIsHTML =
+ window.document.doctype && window.document.doctype.name === "html";
+ const suffixIsNotXMLorPDF =
+ !window.location.pathname.endsWith(".xml") &&
+ !window.location.pathname.endsWith(".pdf");
+ const rootElementIsHTML =
+ document.documentElement.nodeName &&
+ document.documentElement.nodeName.toLowerCase() === "html";
+ const pageAcceptsTalerSupport = document.head.querySelector(
+ "meta[name=taler-support]",
+ );
+
+ // this is also checked by the loader
+ // but a double check will prevent running and breaking user navigation
+ // if loaded from other location
+ const shouldNotRun =
+ !documentDocTypeIsHTML ||
+ !suffixIsNotXMLorPDF ||
+ !pageAcceptsTalerSupport ||
+ !rootElementIsHTML;
+
+ interface Info {
+ extensionId: string;
+ protocol: string;
+ hostname: string;
+ }
+ interface API {
+ convertURIToWebExtensionPath: (uri: string) => string | undefined;
+ anchorOnClick: (ev: MouseEvent) => void;
+ registerProtocolHandler: () => void;
+ }
+ interface TalerSupport {
+ info: Readonly<Info>;
+ __internal: API;
+ }
+
+ function buildApi(config: Readonly<Info>): API {
+ /**
+ * Takes an anchor href that starts with taler:// and
+ * returns the path to the web-extension page
+ */
+ function convertURIToWebExtensionPath(uri: string): string | undefined {
+ if (!validateTalerUri(uri)) {
+ logger.error(`taler:// URI is invalid: ${uri}`);
+ return undefined;
+ }
+ const host = `${config.protocol}//${config.hostname}`;
+ const path = `static/wallet.html#/taler-uri/${encodeURIComponent(uri)}`;
+ return `${host}/${path}`;
+ }
+
+ function anchorOnClick(ev: MouseEvent) {
+ if (!(ev.currentTarget instanceof Element)) {
+ logger.debug(`onclick: registered in a link that is not an HTML element`);
+ return;
+ }
+ const hrefAttr = ev.currentTarget.attributes.getNamedItem("href");
+ if (!hrefAttr) {
+ logger.debug(`onclick: link didn't have href with taler:// uri`);
+ return;
+ }
+ const targetAttr = ev.currentTarget.attributes.getNamedItem("target");
+ const windowTarget =
+ targetAttr && targetAttr.value ? targetAttr.value : "_self";
+ const page = convertURIToWebExtensionPath(hrefAttr.value);
+ if (!page) {
+ logger.debug(`onclick: could not convert "${hrefAttr.value}" into path`);
+ return;
+ }
+ // we can use window.open, but maybe some browser will block it?
+ window.open(page, windowTarget);
+ ev.preventDefault();
+ ev.stopPropagation();
+ ev.stopImmediatePropagation();
+ return false;
+ }
+
+ function overrideAllAnchor(root: HTMLElement) {
+ const allAnchors = root.querySelectorAll("a[href^=taler]");
+ logger.debug(`registering taler protocol in ${allAnchors.length} links`);
+ allAnchors.forEach((link) => {
+ if (link instanceof HTMLElement) {
+ link.addEventListener("click", anchorOnClick);
+ }
+ });
+ }
+
+ function checkForNewAnchors(
+ mutations: MutationRecord[],
+ observer: MutationObserver,
+ ) {
+ mutations.forEach((mut) => {
+ if (mut.type === "childList") {
+ mut.addedNodes.forEach((added) => {
+ if (added instanceof HTMLElement) {
+ logger.debug(`new element`, added);
+ overrideAllAnchor(added);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Check of every anchor and observes for new one.
+ * Register the anchor handler when found
+ */
+ function registerProtocolHandler() {
+ if (document.body) overrideAllAnchor(document.body)
+ new MutationObserver(checkForNewAnchors).observe(document, {
+ childList: true,
+ subtree: true,
+ attributes: false,
+ });
+ }
+
+ return {
+ convertURIToWebExtensionPath,
+ anchorOnClick,
+ registerProtocolHandler,
+ };
+ }
+
+ function start() {
+ if (shouldNotRun) return;
+ if (!(document.currentScript instanceof HTMLScriptElement)) return;
+
+ const url = new URL(document.currentScript.src);
+ const { protocol, searchParams, hostname } = url;
+ const extensionId = searchParams.get("id") ?? "";
+ const debugEnabled = searchParams.get("debug") === "true";
+ const apiEnabled = searchParams.get("api") === "true";
+ const hijackEnabled = searchParams.get("hijack") === "true";
+
+ const info: Info = Object.freeze({
+ extensionId,
+ protocol,
+ hostname,
+ });
+
+ if (debugEnabled) {
+ logger.debug = logger.info;
+ }
+
+ const taler: TalerSupport = {
+ info,
+ __internal: buildApi(info),
+ };
+
+ if (apiEnabled) {
+ //@ts-ignore
+ window.taler = taler;
+ }
+
+ if (hijackEnabled) {
+ taler.__internal.registerProtocolHandler();
+ }
+ }
+
+ // utils functions
+ function validateTalerUri(uri: string): boolean {
+ return (
+ !!uri && (uri.startsWith("taler://") || uri.startsWith("taler+http://"))
+ );
+ }
+
+ start();
+})()
+
diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts
index 6bf1be3ff..452cc578e 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,15 +14,201 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ComponentChildren, FunctionalComponent, h as render } from 'preact';
+import { NotificationType, TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient, WalletNotification } from "@gnu-taler/taler-util";
+import {
+ WalletCoreApiClient,
+ WalletCoreOpKeys,
+ WalletCoreRequestType,
+ WalletCoreResponseType,
+} from "@gnu-taler/taler-wallet-core";
+import { ApiContextProvider, TranslationProvider, defaultRequestHandler } from "@gnu-taler/web-util/browser";
+import {
+ ComponentChildren,
+ FunctionalComponent,
+ VNode,
+ h as create,
+} from "preact";
+import { AlertProvider } from "./context/alert.js";
+import { BackendProvider } from "./context/backend.js";
+import { strings } from "./i18n/strings.js";
+import { nullFunction } from "./mui/handlers.js";
+import { BackgroundApiClient, wxApi } from "./wxApi.js";
-export function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
- const r = (args: any) => render(Component, args)
- r.args = props
- return r
+// export const nullFunction: any = () => null;
+
+interface MockHandler {
+ addWalletCallResponse<Op extends WalletCoreOpKeys>(
+ operation: Op,
+ payload?: Partial<WalletCoreRequestType<Op>>,
+ response?: WalletCoreResponseType<Op>,
+ callback?: () => void,
+ ): MockHandler;
+
+ getCallingQueueState(): "empty" | string;
+
+ notifyEventFromWallet(notif: WalletNotification): void;
}
+type CallRecord = WalletCallRecord | BackgroundCallRecord;
+interface WalletCallRecord {
+ source: "wallet";
+ callback: () => void;
+ operation: WalletCoreOpKeys;
+ payload?: WalletCoreRequestType<WalletCoreOpKeys>;
+ response?: WalletCoreResponseType<WalletCoreOpKeys>;
+}
+interface BackgroundCallRecord {
+ source: "background";
+ name: string;
+ args: any;
+ response: any;
+}
+
+type Subscriptions = {
+ [key in NotificationType]?: (d: WalletNotification) => void;
+};
+
+export function createWalletApiMock(): {
+ handler: MockHandler;
+ TestingContext: FunctionalComponent<{ children: ComponentChildren }>;
+} {
+ const calls = new Array<CallRecord>();
+ const subscriptions: Subscriptions = {};
+
+ const mock: typeof wxApi = {
+ wallet: new Proxy<WalletCoreApiClient>({} as any, {
+ get(target, name, receiver) {
+ const functionName = String(name);
+ if (functionName !== "call") {
+ throw Error(
+ `the only method in wallet api should be 'call': ${functionName}`,
+ );
+ }
+ return function (
+ operation: WalletCoreOpKeys,
+ payload: WalletCoreRequestType<WalletCoreOpKeys>,
+ ) {
+ const next = calls.shift();
+
+ if (!next) {
+ throw Error(
+ `wallet operation was called but none was expected: ${operation} (${JSON.stringify(
+ payload,
+ undefined,
+ 2,
+ )})`,
+ );
+ }
+ if (next.source !== "wallet") {
+ throw Error(`wallet operation expected`);
+ }
+ if (operation !== next.operation) {
+ //more checks, deep check payload
+ throw Error(
+ `wallet operation doesn't match: expected ${next.operation} actual ${operation}`,
+ );
+ }
+ next.callback();
+
+ return next.response ?? {};
+ };
+ },
+ }),
+ listener: {
+ trigger: () => {
+
+ },
+ onUpdateNotification(
+ mTypes: NotificationType[],
+ callback: ((d: WalletNotification) => void) | undefined,
+ ): () => void {
+ mTypes.forEach((m) => {
+ subscriptions[m] = callback;
+ });
+ return nullFunction;
+ },
+ },
+ background: new Proxy<BackgroundApiClient>({} as any, {
+ get(target, name, receiver) {
+ const functionName = String(name);
+ return function (...args: any) {
+ const next = calls.shift();
+ if (!next) {
+ throw Error(
+ `background operation was called but none was expected: ${functionName} (${JSON.stringify(
+ args,
+ undefined,
+ 2,
+ )})`,
+ );
+ }
+ if (next.source !== "background" || functionName !== next.name) {
+ //more checks, deep check args
+ throw Error(`background operation doesn't match`);
+ }
+ return next.response;
+ };
+ },
+ }),
+ };
+
+ const handler: MockHandler = {
+ addWalletCallResponse(operation, payload, response, cb) {
+ calls.push({
+ source: "wallet",
+ operation,
+ payload,
+ response,
+ callback: cb
+ ? cb
+ : () => {
+ null;
+ },
+ });
+ return handler;
+ },
+ notifyEventFromWallet(event: WalletNotification): void {
+ const callback = subscriptions[event.type];
+ if (!callback)
+ throw Error(`Expected to have a subscription for ${event}`);
+ return callback(event);
+ },
+ getCallingQueueState() {
+ return calls.length === 0 ? "empty" : `${calls.length} left`;
+ },
+ };
+
+ function TestingContext({
+ children: _cs,
+ }: {
+ children: ComponentChildren;
+ }): VNode {
+ let children = _cs;
+ children = create(AlertProvider, { children }, children);
+ const value = {
+ request: defaultRequestHandler,
+ bankCore: new TalerCoreBankHttpClient("/"),
+ bankIntegration: new TalerBankIntegrationHttpClient("/"),
+ bankWire: new TalerWireGatewayHttpClient("/",""),
+ bankRevenue: new TalerRevenueHttpClient("/"),
+ }
+ children = create(ApiContextProvider, { value, children }, children);
+ children = create(
+ TranslationProvider,
+ { children, source: strings, initial: "en", forceLang: "en" },
+ children,
+ );
+ return create(
+ BackendProvider,
+ {
+ wallet: mock.wallet,
+ background: mock.background,
+ listener: mock.listener,
+ children,
+ },
+ children,
+ );
+ }
-export function NullLink({ children }: { children?: ComponentChildren }) {
- return render('a', { children, href: 'javascript:void(0);' })
+ return { handler, TestingContext };
}
diff --git a/packages/taler-wallet-webextension/src/utils/index.ts b/packages/taler-wallet-webextension/src/utils/index.ts
new file mode 100644
index 000000000..d83e6f472
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/utils/index.ts
@@ -0,0 +1,119 @@
+/*
+ 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 { createElement, VNode } from "preact";
+import { useCallback, useMemo } from "preact/hooks";
+
+function getJsonIfOk(r: Response): Promise<any> {
+ if (r.ok) {
+ return r.json();
+ }
+
+ if (r.status >= 400 && r.status < 500) {
+ throw new Error(`URL may not be right: (${r.status}) ${r.statusText}`);
+ }
+
+ throw new Error(
+ `Try another server: (${r.status}) ${r.statusText || "internal server error"
+ }`,
+ );
+}
+
+export async function queryToSlashConfig<T>(url: string): Promise<T> {
+ return fetch(new URL("config", url).href)
+ .catch(() => {
+ throw new Error(`Network error`);
+ })
+ .then(getJsonIfOk);
+}
+
+function timeout<T>(ms: number, promise: Promise<T>): Promise<T> {
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ reject(
+ new Error(
+ `Timeout: the query took longer than ${Math.floor(ms / 1000)} secs`,
+ ),
+ );
+ }, ms);
+
+ promise
+ .then((value) => {
+ clearTimeout(timer);
+ resolve(value);
+ })
+ .catch((reason) => {
+ clearTimeout(timer);
+ reject(reason);
+ });
+ });
+}
+
+export async function queryToSlashKeys<T>(url: string): Promise<T> {
+ const endpoint = new URL("keys", url);
+
+ const query = fetch(endpoint.href)
+ .catch(() => {
+ throw new Error(`Network error`);
+ })
+ .then(getJsonIfOk);
+
+ return timeout(3000, query);
+}
+
+export type StateFunc<S> = (p: S) => VNode | null;
+
+export type StateViewMap<StateType extends { status: string }> = {
+ [S in StateType as S["status"]]: StateFunc<S>;
+};
+
+export type RecursiveState<S extends object> = S | (() => RecursiveState<S>);
+
+export function compose<SType extends { status: string }, PType>(
+ name: string,
+ hook: (p: PType) => RecursiveState<SType>,
+ viewMap: StateViewMap<SType>,
+): (p: PType) => VNode {
+ function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
+ function TheComponent(): VNode {
+ //if the function is the same, do not compute
+ const state = stateHook();
+
+ if (typeof state === "function") {
+ const subComponent = withHook(state);
+ return createElement(subComponent, {});
+ }
+
+ const statusName = state.status as unknown as SType["status"];
+ const viewComponent = viewMap[statusName] as unknown as StateFunc<SType>;
+ return createElement(viewComponent, state);
+ }
+ // TheComponent.name = `${name}`;
+
+ return useMemo(() => {
+ return TheComponent
+ }, [stateHook]);
+ }
+
+ return (p: PType) => {
+ const h = withHook(() => hook(p));
+ return h();
+ };
+}
+
+export function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
new file mode 100644
index 000000000..daa6b425d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
@@ -0,0 +1,88 @@
+/*
+ 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 {
+ SyncTermsOfServiceResponse,
+ TalerErrorDetail,
+} from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import {
+ ButtonHandler,
+ TextFieldHandler,
+ ToggleHandler,
+} from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ConfirmProviderView, SelectProviderView } from "./views.js";
+
+export interface Props {
+ onBack: () => Promise<void>;
+ onComplete: (pid: string) => Promise<void>;
+ onPaymentRequired: (uri: string) => Promise<void>;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | State.ConfirmProvider
+ | State.SelectProvider;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface ConfirmProvider {
+ status: "confirm-provider";
+ error: undefined;
+ url: string;
+ provider: SyncTermsOfServiceResponse;
+ tos: ToggleHandler;
+ onCancel: ButtonHandler;
+ onAccept: ButtonHandler;
+ }
+
+ export interface SelectProvider {
+ status: "select-provider";
+ url: TextFieldHandler;
+ urlOk: boolean;
+ name: TextFieldHandler;
+ onConfirm: ButtonHandler;
+ onCancel: ButtonHandler;
+ error: undefined | TalerErrorDetail;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ "select-provider": SelectProviderView,
+ "confirm-provider": ConfirmProviderView,
+};
+
+export const AddBackupProviderPage = compose(
+ "AddBackupProvider",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
new file mode 100644
index 000000000..75b8e53c0
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
@@ -0,0 +1,263 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ canonicalizeBaseUrl,
+ Codec,
+ codecForSyncTermsOfServiceResponse,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useEffect, useState } from "preact/hooks";
+import { useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { assertUnreachable } from "../../utils/index.js";
+import { Props, State } from "./index.js";
+
+type UrlState<T> = UrlOk<T> | UrlError;
+
+interface UrlOk<T> {
+ status: "ok";
+ result: T;
+}
+type UrlError =
+ | UrlNetworkError
+ | UrlClientError
+ | UrlServerError
+ | UrlParsingError
+ | UrlReadError;
+
+interface UrlNetworkError {
+ status: "network-error";
+ href: string;
+}
+interface UrlClientError {
+ status: "client-error";
+ code: number;
+}
+interface UrlServerError {
+ status: "server-error";
+ code: number;
+}
+interface UrlParsingError {
+ status: "parsing-error";
+ json: any;
+}
+interface UrlReadError {
+ status: "url-error";
+}
+
+function useDebounceEffect(
+ time: number,
+ cb: undefined | (() => Promise<void>),
+ deps: Array<any>,
+): void {
+ const [currentTimer, setCurrentTimer] = useState<any>();
+ useEffect(() => {
+ if (currentTimer !== undefined) clearTimeout(currentTimer);
+ if (cb !== undefined) {
+ const tid = setTimeout(cb, time);
+ setCurrentTimer(tid);
+ }
+ }, deps);
+}
+
+function useUrlState<T>(
+ host: string | undefined,
+ path: string,
+ codec: Codec<T>,
+): UrlState<T> | undefined {
+ const [state, setState] = useState<UrlState<T> | undefined>();
+
+ let href: string | undefined;
+ try {
+ if (host) {
+ const isHttps =
+ host.startsWith("https://") && host.length > "https://".length;
+ const isHttp =
+ host.startsWith("http://") && host.length > "http://".length;
+ const withProto = isHttp || isHttps ? host : `https://${host}`;
+ const baseUrl = canonicalizeBaseUrl(withProto);
+ href = new URL(path, baseUrl).href;
+ }
+ } catch (e) {
+ setState({
+ status: "url-error",
+ });
+ }
+ const constHref = href;
+
+ async function checkURL() {
+ if (!constHref) {
+ return;
+ }
+ const req = await fetch(constHref).catch((e) => {
+ return setState({
+ status: "network-error",
+ href: constHref,
+ });
+ });
+ if (!req) return;
+
+ if (req.status >= 400 && req.status < 500) {
+ setState({
+ status: "client-error",
+ code: req.status,
+ });
+ return;
+ }
+ if (req.status > 500) {
+ setState({
+ status: "server-error",
+ code: req.status,
+ });
+ return;
+ }
+
+ const json = await req.json();
+ try {
+ const result = codec.decode(json);
+ setState({ status: "ok", result });
+ } catch (e: any) {
+ setState({ status: "parsing-error", json });
+ }
+ }
+
+ useDebounceEffect(
+ 500,
+ constHref == undefined ? undefined : checkURL,
+ [host, path],
+ );
+
+ return state;
+}
+
+export function useComponentState({
+ onBack,
+ onComplete,
+ onPaymentRequired,
+}: Props): State {
+ const api = useBackendContext();
+ const [url, setHost] = useState<string | undefined>();
+ const [name, setName] = useState<string | undefined>();
+ const [tos, setTos] = useState(false);
+ const { pushAlertOnError } = useAlertContext();
+ const urlState = useUrlState(
+ url,
+ "config",
+ codecForSyncTermsOfServiceResponse(),
+ );
+ const [showConfirm, setShowConfirm] = useState(false);
+
+ async function addBackupProvider(): Promise<void> {
+ if (!url || !name) return;
+
+ const resp = await api.wallet.call(WalletApiOperation.AddBackupProvider, {
+ backupProviderBaseUrl: url,
+ name: name,
+ activate: true,
+ });
+
+ switch (resp.status) {
+ case "payment-required":
+ if (resp.talerUri) {
+ return onPaymentRequired(resp.talerUri);
+ } else {
+ return onComplete(url);
+ }
+ case "ok":
+ return onComplete(url);
+ default:
+ assertUnreachable(resp);
+ }
+ }
+
+ if (showConfirm && urlState && urlState.status === "ok") {
+ return {
+ status: "confirm-provider",
+ error: undefined,
+ onAccept: {
+ onClick: !tos ? undefined : pushAlertOnError(addBackupProvider),
+ },
+ onCancel: {
+ onClick: pushAlertOnError(onBack),
+ },
+ provider: urlState.result,
+ tos: {
+ value: tos,
+ button: {
+ onClick: pushAlertOnError(async () => setTos(!tos)),
+ },
+ },
+ url: url ?? "",
+ };
+ }
+
+ return {
+ status: "select-provider",
+ error: undefined,
+ name: {
+ value: name || "",
+ onInput: pushAlertOnError(async (e) => setName(e)),
+ error:
+ name === undefined ? undefined : !name ? "Can't be empty" : undefined,
+ },
+ onCancel: {
+ onClick: pushAlertOnError(onBack),
+ },
+ onConfirm: {
+ onClick:
+ !urlState || urlState.status !== "ok" || !name
+ ? undefined
+ : pushAlertOnError(async () => {
+ setShowConfirm(true);
+ }),
+ },
+ urlOk: urlState?.status === "ok",
+ url: {
+ value: url || "",
+ onInput: pushAlertOnError(async (e) => setHost(e)),
+ error: errorString(urlState),
+ },
+ };
+}
+
+function errorString(state: undefined | UrlState<any>): string | undefined {
+ if (!state) return state;
+ switch (state.status) {
+ case "ok":
+ return undefined;
+ case "client-error": {
+ switch (state.code) {
+ case 404:
+ return "Not found";
+ case 401:
+ return "Unauthorized";
+ case 403:
+ return "Forbidden";
+ default:
+ return `Server says it a client error: ${state.code}.`;
+ }
+ }
+ case "server-error":
+ return `Server had a problem ${state.code}.`;
+ case "parsing-error":
+ return `Server response doesn't have the right format.`;
+ case "network-error":
+ return `Unable to connect to ${state.href}.`;
+ case "url-error":
+ return "URL is not complete";
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx
new file mode 100644
index 000000000..7ac92c6c9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/stories.tsx
@@ -0,0 +1,110 @@
+/*
+ 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 { ConfirmProviderView, SelectProviderView } from "./views.js";
+import { AmountString } from "@gnu-taler/taler-util";
+
+export default {
+ title: "add backup provider",
+};
+
+export const DemoService = tests.createExample(ConfirmProviderView, {
+ url: "https://sync.demo.taler.net/",
+ provider: {
+ annual_fee: "KUDOS:0.1" as AmountString,
+ storage_limit_in_megabytes: 20,
+ version: "1",
+ },
+ tos: {
+ button: {},
+ },
+ onAccept: {},
+ onCancel: {},
+});
+
+export const FreeService = tests.createExample(ConfirmProviderView, {
+ url: "https://sync.taler:9667/",
+ provider: {
+ annual_fee: "ARS:0" as AmountString,
+ storage_limit_in_megabytes: 20,
+ version: "1",
+ },
+ tos: {
+ button: {},
+ },
+ onAccept: {},
+ onCancel: {},
+});
+
+export const Initial = tests.createExample(SelectProviderView, {
+ url: { value: "" },
+ name: { value: "" },
+ onCancel: {},
+ onConfirm: {},
+});
+
+export const WithValue = tests.createExample(SelectProviderView, {
+ url: {
+ value: "sync.demo.taler.net",
+ },
+ name: {
+ value: "Demo backup service",
+ },
+ onCancel: {},
+ onConfirm: {},
+});
+
+export const WithConnectionError = tests.createExample(SelectProviderView, {
+ url: {
+ value: "sync.demo.taler.net",
+ error: "Network error",
+ },
+ name: {
+ value: "Demo backup service",
+ },
+ onCancel: {},
+ onConfirm: {},
+});
+
+export const WithClientError = tests.createExample(SelectProviderView, {
+ url: {
+ value: "sync.demo.taler.net",
+ error: "URL may not be right: (404) Not Found",
+ },
+ name: {
+ value: "Demo backup service",
+ },
+ onCancel: {},
+ onConfirm: {},
+});
+
+export const WithServerError = tests.createExample(SelectProviderView, {
+ url: {
+ value: "sync.demo.taler.net",
+ error: "Try another server: (500) Internal Server Error",
+ },
+ name: {
+ value: "Demo backup service",
+ },
+ onCancel: {},
+ onConfirm: {},
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
new file mode 100644
index 000000000..058f4f460
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+
+const props: Props = {
+ onBack: nullFunction,
+ onComplete: nullFunction,
+ onPaymentRequired: nullFunction,
+};
+describe("AddBackupProvider states", () => {
+ /**
+ * FIXME: this test has inconsistent behavior.
+ * it should always expect one state but for some reason
+ * (maybe race condition) it sometime expect 1 update when
+ * it should no update
+ */
+ it.skip("should start in 'select-provider' state", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ (state) => {
+ expect(state.status).equal("select-provider");
+ if (state.status !== "select-provider") return;
+ expect(state.name.value).eq("");
+ expect(state.url.value).eq("");
+ },
+ //FIXME: this shouldn't take 2 updates, just
+ // (state) => {
+ // expect(state.status).equal("select-provider");
+ // if (state.status !== "select-provider") return;
+ // expect(state.name.value).eq("");
+ // expect(state.url.value).eq("");
+ // },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/views.tsx b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/views.tsx
new file mode 100644
index 000000000..c67c288dc
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/views.tsx
@@ -0,0 +1,158 @@
+/*
+ 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 { Fragment, h, VNode } from "preact";
+import { Checkbox } from "../../components/Checkbox.js";
+import {
+ LightText,
+ SmallLightText,
+ SubTitle,
+ Title,
+} from "../../components/styled/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Button } from "../../mui/Button.js";
+import { TextField } from "../../mui/TextField.js";
+import { State } from "./index.js";
+
+export function ConfirmProviderView({
+ url,
+ provider,
+ tos,
+ onCancel,
+ onAccept,
+}: State.ConfirmProvider): VNode {
+ const { i18n } = useTranslationContext();
+ const noFee = Amounts.isZero(provider.annual_fee);
+ return (
+ <Fragment>
+ <section>
+ <Title>
+ <i18n.Translate>Review terms of service</i18n.Translate>
+ </Title>
+ <div>
+ <i18n.Translate>Provider URL</i18n.Translate>:{" "}
+ <a href={url} target="_blank" rel="noreferrer">
+ {url}
+ </a>
+ </div>
+ <SmallLightText>
+ <i18n.Translate>
+ Please review and accept this provider&apos;s terms of service
+ </i18n.Translate>
+ </SmallLightText>
+ <SubTitle>
+ 1. <i18n.Translate>Pricing</i18n.Translate>
+ </SubTitle>
+ <p>
+ {noFee ? (
+ <i18n.Translate>free of charge</i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ {provider.annual_fee} per year of service
+ </i18n.Translate>
+ )}
+ </p>
+ <SubTitle>
+ 2. <i18n.Translate>Storage</i18n.Translate>
+ </SubTitle>
+ <p>
+ <i18n.Translate>
+ {provider.storage_limit_in_megabytes} megabytes of storage per year
+ of service
+ </i18n.Translate>
+ </p>
+ <Checkbox
+ label={i18n.str`Accept terms of service`}
+ name="terms"
+ onToggle={tos.button.onClick}
+ enabled={tos.value}
+ />
+ </section>
+ <footer>
+ <Button
+ variant="contained"
+ color="secondary"
+ onClick={onCancel.onClick}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ <Button variant="contained" color="primary" onClick={onAccept.onClick}>
+ {noFee ? (
+ <i18n.Translate>Add provider</i18n.Translate>
+ ) : (
+ <i18n.Translate>Pay</i18n.Translate>
+ )}
+ </Button>
+ </footer>
+ </Fragment>
+ );
+}
+
+export function SelectProviderView({
+ url,
+ name,
+ urlOk,
+ onCancel,
+ onConfirm,
+}: State.SelectProvider): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <section>
+ <Title>
+ <i18n.Translate>Add backup provider</i18n.Translate>
+ </Title>
+ <LightText>
+ <i18n.Translate>
+ Backup providers may charge for their service
+ </i18n.Translate>
+ </LightText>
+ <p>
+ <TextField
+ label={<i18n.Translate>URL</i18n.Translate>}
+ placeholder="https://"
+ color={urlOk ? "success" : undefined}
+ value={url.value}
+ error={url.error}
+ onChange={url.onInput}
+ />
+ </p>
+ <p>
+ <TextField
+ label={<i18n.Translate>Name</i18n.Translate>}
+ placeholder="provider name"
+ value={name.value}
+ error={name.error}
+ onChange={name.onInput}
+ />
+ </p>
+ </section>
+ <footer>
+ <Button
+ variant="contained"
+ color="secondary"
+ onClick={onCancel.onClick}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ <Button variant="contained" color="primary" onClick={onConfirm.onClick}>
+ <i18n.Translate>Next</i18n.Translate>
+ </Button>
+ </footer>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
new file mode 100644
index 000000000..94b32c157
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/index.ts
@@ -0,0 +1,92 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { OperationFailWithBody, OperationOk, TalerExchangeApi } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { TextFieldHandler } from "../../mui/handlers.js";
+import { StateViewMap, compose } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ConfirmAddExchangeView, VerifyView } from "./views.js";
+
+export interface Props {
+ currency?: string;
+ onBack: () => Promise<void>;
+ noDebounce?: boolean;
+}
+
+export type State = State.Loading
+ | State.LoadingUriError
+ | State.Confirm
+ | State.Verify;
+
+export type CheckExchangeErrors = {
+ "invalid-version": string;
+ "invalid-currency": string;
+ "not-found": void;
+ "already-active": void;
+ "invalid-protocol": void;
+}
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+ export interface Confirm extends BaseInfo {
+ status: "confirm";
+ url: string;
+ onCancel: () => Promise<void>;
+ onConfirm: () => Promise<void>;
+ error: undefined;
+ }
+ export interface Verify extends BaseInfo {
+ status: "verify";
+ error: undefined;
+
+ onCancel: () => Promise<void>;
+ onAccept: () => Promise<void>;
+
+ url: TextFieldHandler,
+ loading: boolean;
+ knownExchanges: URL[],
+ result: OperationOk<TalerExchangeApi.ExchangeKeysResponse> | OperationFailWithBody<CheckExchangeErrors> | undefined,
+ expectedCurrency: string | undefined,
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ confirm: ConfirmAddExchangeView,
+ verify: VerifyView,
+};
+
+export const AddExchange = compose(
+ "AddExchange",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
new file mode 100644
index 000000000..4a04f762a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
@@ -0,0 +1,198 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ExchangeEntryStatus, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailureWithBody } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useBackendContext } from "../../context/backend.js";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { withSafe } from "../../mui/handlers.js";
+import { RecursiveState } from "../../utils/index.js";
+import { CheckExchangeErrors, Props, State } from "./index.js";
+
+function urlFromInput(str: string): URL {
+ let result: URL;
+ try {
+ result = new URL(str)
+ } catch (original) {
+ try {
+ result = new URL(`https://${str}`)
+ } catch (e) {
+ throw original
+ }
+ }
+ if (!result.pathname.endsWith("/")) {
+ result.pathname = result.pathname + "/";
+ }
+ result.search = "";
+ result.hash = "";
+ return result;
+}
+
+export function useComponentState({ onBack, currency, noDebounce }: Props): RecursiveState<State> {
+ const [verified, setVerified] = useState<string>();
+
+ const api = useBackendContext();
+ const hook = useAsyncAsHook(() =>
+ api.wallet.call(WalletApiOperation.ListExchanges, {}),
+ );
+ const walletExchanges = !hook ? [] : hook.hasError ? [] : hook.response.exchanges
+ const used = walletExchanges.filter(e => e.exchangeEntryStatus === ExchangeEntryStatus.Used);
+ const preset = walletExchanges.filter(e => e.exchangeEntryStatus === ExchangeEntryStatus.Preset);
+
+ if (!verified) {
+ return (): State => {
+ const checkExchangeBaseUrl_memo = useCallback(async function checkExchangeBaseUrl(str: string) {
+ const baseUrl = urlFromInput(str)
+ if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
+ return opKnownFailureWithBody<CheckExchangeErrors>("invalid-protocol", undefined)
+ }
+ const found = used.findIndex((e) => e.exchangeBaseUrl === baseUrl.href);
+ if (found !== -1) {
+ return opKnownFailureWithBody<CheckExchangeErrors>("already-active", undefined);
+ }
+
+ /**
+ * FIXME: For some reason typescript doesn't like the next BrowserFetchHttpLib
+ *
+ * │ src/wallet/AddExchange/state.ts(68,63): error TS2345: Argument of type 'BrowserFetchHttpLib' is not assignable to parameter of ty
+ * │ Types of property 'fetch' are incompatible.
+ * │ Type '(requestUrl: string, options?: HttpRequestOptions | undefined) => Promise<HttpResponse>' is not assignable to type '(ur
+ * │ Types of parameters 'options' and 'opt' are incompatible.
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/http-common", { wi
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/http-common", {
+ * │ Types of property 'cancellationToken' are incompatible.
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/Cancellation
+ * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/Cancellati
+ * │ Types have separate declarations of a private property '_isCancelled'.
+ *
+ */
+ const api = new TalerExchangeHttpClient(baseUrl.href, new BrowserFetchHttpLib() as any);
+ const config = await api.getConfig()
+ if (config.type === "fail") {
+ return opKnownFailureWithBody<CheckExchangeErrors>("not-found", undefined)
+ }
+ if (!api.isCompatible(config.body.version)) {
+ return opKnownFailureWithBody<CheckExchangeErrors>("invalid-version", config.body.version)
+ }
+ if (currency !== undefined && currency !== config.body.currency) {
+ return opKnownFailureWithBody<CheckExchangeErrors>("invalid-currency", config.body.currency)
+ }
+ const keys = await api.getKeys()
+ return keys
+ }, [used])
+
+ const { result, value: url, loading, update, error: requestError } = useDebounce(checkExchangeBaseUrl_memo, noDebounce ?? false)
+ const [inputError, setInputError] = useState<string>()
+
+ return {
+ status: "verify",
+ error: undefined,
+ onCancel: onBack,
+ expectedCurrency: currency,
+ onAccept: async () => {
+ if (!result || result.type !== "ok") return;
+ setVerified(result.body.base_url)
+ },
+ result,
+ loading,
+ knownExchanges: preset.map(e => new URL(e.exchangeBaseUrl)),
+ url: {
+ value: url ?? "",
+ error: inputError ?? requestError,
+ onInput: withSafe(update, (e) => {
+ setInputError(e.message)
+ })
+ },
+ };
+ }
+ }
+
+ async function onConfirm() {
+ if (!verified) return;
+ await api.wallet.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: canonicalizeBaseUrl(verified),
+ forceUpdate: true,
+ });
+ onBack();
+ }
+
+ return {
+ status: "confirm",
+ error: undefined,
+ onCancel: onBack,
+ onConfirm,
+ url: verified
+ };
+}
+
+
+
+function useDebounce<T>(
+ onTrigger: (v: string) => Promise<T>,
+ disabled: boolean,
+): {
+ loading: boolean;
+ error?: Error;
+ value: string | undefined;
+ result: T | undefined;
+ update: (s: string) => void;
+} {
+ const [value, setValue] = useState<string>();
+ const [dirty, setDirty] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [result, setResult] = useState<T | undefined>(undefined);
+ const [error, setError] = useState<Error | undefined>(undefined);
+
+ const [handler, setHandler] = useState<number | undefined>(undefined);
+
+ if (!disabled) {
+ useEffect(() => {
+ if (!value) return;
+ clearTimeout(handler);
+ const h = setTimeout(async () => {
+ setDirty(true);
+ setLoading(true);
+ try {
+ const result = await onTrigger(value);
+ setResult(result);
+ setError(undefined);
+ setLoading(false);
+ } catch (er) {
+ if (er instanceof Error) {
+ setError(er);
+ } else {
+ // @ts-expect-error cause still not in typescript
+ setError(new Error('unkown error on debounce', { cause: er }))
+ }
+ setLoading(false);
+ setResult(undefined);
+ }
+ }, 500);
+ setHandler(h as unknown as number);
+ }, [value, setHandler, onTrigger]);
+ }
+
+ return {
+ error: dirty ? error : undefined,
+ loading: loading,
+ result: result,
+ value: value,
+ update: disabled ? onTrigger : setValue,
+ };
+}
+
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx b/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx
new file mode 100644
index 000000000..f205b6415
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/stories.tsx
@@ -0,0 +1,27 @@
+/*
+ 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)
+ */
+
+
+export default {
+ title: "example",
+};
+
+// export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
new file mode 100644
index 000000000..d0e78a94e
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/test.ts
@@ -0,0 +1,209 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ ExchangeEntryStatus,
+ ExchangeTosStatus,
+ ExchangeUpdateStatus,
+ ScopeType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+const props: Props = {
+ onBack: nullFunction,
+ noDebounce: true,
+};
+
+describe("AddExchange states", () => {
+ it("should start in 'verify' state", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.ListExchanges,
+ {},
+ {
+ exchanges: [
+ {
+ exchangeBaseUrl: "http://exchange.local/",
+ ageRestrictionOptions: [],
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://exchange.local/",
+ },
+ masterPub: "123qwe123",
+ currency: "ARS",
+ exchangeEntryStatus: ExchangeEntryStatus.Ephemeral,
+ tosStatus: ExchangeTosStatus.Pending,
+ exchangeUpdateStatus: ExchangeUpdateStatus.UnavailableUpdate,
+ paytoUris: [],
+ lastUpdateTimestamp: undefined,
+ noFees: false,
+ peerPaymentsDisabled: false,
+ },
+ ],
+ },
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ (state) => {
+ expect(state.status).equal("verify");
+ if (state.status !== "verify") return;
+ expect(state.url.value).eq("");
+ expect(state.expectedCurrency).is.undefined;
+ expect(state.result).is.undefined;
+ },
+ (state) => {
+ expect(state.status).equal("verify");
+ if (state.status !== "verify") return;
+ expect(state.url.value).eq("");
+ expect(state.expectedCurrency).is.undefined;
+ expect(state.result).is.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ // it("should not be able to add a known exchange", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.ListExchanges,
+ // {},
+ // {
+ // exchanges: [
+ // {
+ // exchangeBaseUrl: "http://exchange.local/",
+ // ageRestrictionOptions: [],
+ // scopeInfo: undefined,
+ // currency: "ARS",
+ // exchangeEntryStatus: ExchangeEntryStatus.Used,
+ // tosStatus: ExchangeTosStatus.Pending,
+ // exchangeUpdateStatus: ExchangeUpdateStatus.Ready,
+ // paytoUris: [],
+ // },
+ // ],
+ // },
+ // );
+
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // },
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // expect(state.error).is.undefined;
+ // expect(state.url.onInput).is.not.undefined;
+ // if (!state.url.onInput) return;
+ // state.url.onInput("http://exchange.local/");
+ // },
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // expect(state.url.error).eq("This exchange is already active");
+ // expect(state.url.onInput).is.not.undefined;
+ // },
+ // ],
+ // TestingContext,
+ // );
+
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+
+ // it("should be able to add a preset exchange", async () => {
+ // const { handler, TestingContext } = createWalletApiMock();
+
+ // handler.addWalletCallResponse(
+ // WalletApiOperation.ListExchanges,
+ // {},
+ // {
+ // exchanges: [
+ // {
+ // exchangeBaseUrl: "http://exchange.local/",
+ // ageRestrictionOptions: [],
+ // scopeInfo: undefined,
+ // currency: "ARS",
+ // exchangeEntryStatus: ExchangeEntryStatus.Preset,
+ // tosStatus: ExchangeTosStatus.Pending,
+ // exchangeUpdateStatus: ExchangeUpdateStatus.Ready,
+ // paytoUris: [],
+ // },
+ // ],
+ // },
+ // );
+
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // },
+ // (state) => {
+ // expect(state.status).equal("verify");
+ // if (state.status !== "verify") return;
+ // expect(state.url.value).eq("");
+ // expect(state.expectedCurrency).is.undefined;
+ // expect(state.result).is.undefined;
+ // expect(state.error).is.undefined;
+ // expect(state.url.onInput).is.not.undefined;
+ // if (!state.url.onInput) return;
+ // state.url.onInput("http://exchange.local/");
+ // },
+ // ],
+ // TestingContext,
+ // );
+
+ // expect(hookBehavior).deep.equal({ result: "ok" });
+ // expect(handler.getCallingQueueState()).eq("empty");
+ // });
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
new file mode 100644
index 000000000..f6537bc68
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/views.tsx
@@ -0,0 +1,251 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { ErrorMessage } from "../../components/ErrorMessage.js";
+import {
+ Input,
+ LightText,
+ SubTitle,
+ Title,
+ WarningBox,
+} from "../../components/styled/index.js";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
+import { Button } from "../../mui/Button.js";
+import { State } from "./index.js";
+import { assertUnreachable } from "@gnu-taler/taler-util";
+
+export function VerifyView({
+ expectedCurrency,
+ onCancel,
+ onAccept,
+ result,
+ loading,
+ knownExchanges,
+ url,
+}: State.Verify): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <section>
+ {!expectedCurrency ? (
+ <Title>
+ <i18n.Translate>Add new exchange</i18n.Translate>
+ </Title>
+ ) : (
+ <SubTitle>
+ <i18n.Translate>Add exchange for {expectedCurrency}</i18n.Translate>
+ </SubTitle>
+ )}
+ {!result && (
+ <LightText>
+ <i18n.Translate>
+ Enter the URL of an exchange you trust.
+ </i18n.Translate>
+ </LightText>
+ )}
+ {(() => {
+ if (!result) return;
+ if (result.type == "ok") {
+ return (
+ <LightText>
+ <i18n.Translate>
+ An exchange has been found! Review the information and click
+ next
+ </i18n.Translate>
+ </LightText>
+ );
+ }
+ switch (result.case) {
+ case "already-active": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange is already in your list.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "invalid-protocol": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ Only exchange accessible through "http" and "https" are
+ allowed.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "invalid-version": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange protocol version is not supported: "
+ {result.body}".
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "invalid-currency": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ This exchange currency "{result.body}" doesn&apos;t match
+ the expected currency {expectedCurrency}.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ case "not-found": {
+ return (
+ <WarningBox>
+ <i18n.Translate>
+ No exchange found in that URL.
+ </i18n.Translate>
+ </WarningBox>
+ );
+ }
+ default: {
+ assertUnreachable(result.case);
+ }
+ }
+ })()}
+ <p>
+ <Input invalid={result && result.type !== "ok"}>
+ <label>URL</label>
+ <input
+ type="text"
+ placeholder="https://"
+ value={url.value}
+ onInput={(e) => {
+ if (url.onInput) {
+ url.onInput(e.currentTarget.value);
+ }
+ }}
+ />
+ </Input>
+ {loading && (
+ <div>
+ <i18n.Translate>loading</i18n.Translate>...
+ </div>
+ )}
+ {result && result.type === "ok" && (
+ <Fragment>
+ <Input>
+ <label>
+ <i18n.Translate>Version</i18n.Translate>
+ </label>
+ <input type="text" disabled value={result.body.version} />
+ </Input>
+ <Input>
+ <label>
+ <i18n.Translate>Currency</i18n.Translate>
+ </label>
+ <input type="text" disabled value={result.body.currency} />
+ </Input>
+ </Fragment>
+ )}
+ </p>
+ {url.value && url.error && (
+ <ErrorMessage
+ title={i18n.str`Can't use the URL: "${url.value}"`}
+ description={url.error}
+ />
+ )}
+ </section>
+ <footer>
+ <Button variant="contained" color="secondary" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ <Button
+ variant="contained"
+ disabled={!result || result.type !== "ok"}
+ onClick={onAccept}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </Button>
+ </footer>
+ <section>
+ <ul>
+ {knownExchanges.map((ex) => {
+ return (
+ <li key={ex.href}>
+ <a
+ href="#"
+ onClick={(e) => {
+ if (url.onInput) {
+ url.onInput(ex.href);
+ }
+ e.preventDefault();
+ }}
+ >
+ {ex.href}
+ </a>
+ </li>
+ );
+ })}
+ </ul>
+ </section>
+ </Fragment>
+ );
+}
+
+export function ConfirmAddExchangeView({
+ url,
+ onCancel,
+ onConfirm,
+}: State.Confirm): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <section>
+ <Title>
+ <i18n.Translate>Review terms of service</i18n.Translate>
+ </Title>
+ <div>
+ <i18n.Translate>Exchange URL</i18n.Translate>:
+ <a href={url} target="_blank" rel="noreferrer">
+ {url}
+ </a>
+ </div>
+ </section>
+
+ <TermsOfService key="terms" exchangeUrl={url}>
+ <footer>
+ <Button
+ key="cancel"
+ variant="contained"
+ color="secondary"
+ onClick={onCancel}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ <Button
+ key="add"
+ variant="contained"
+ color="success"
+ onClick={onConfirm}
+ >
+ <i18n.Translate>Add exchange</i18n.Translate>
+ </Button>
+ </footer>
+ </TermsOfService>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx
index 06e33c9d3..704f9e9a1 100644
--- a/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,29 +15,19 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { SettingsView as TestedComponent } from './Settings';
+import * as tests from "@gnu-taler/web-util/testing";
+import { AddNewActionView as TestedComponent } from "./AddNewActionView.js";
export default {
- title: 'popup/settings',
+ title: "add new action",
component: TestedComponent,
argTypes: {
setDeviceName: () => Promise.resolve(),
- }
+ },
};
-export const AllOff = createExample(TestedComponent, {
- deviceName: 'this-is-the-device-name',
- setDeviceName: () => Promise.resolve(),
-});
-
-export const OneChecked = createExample(TestedComponent, {
- deviceName: 'this-is-the-device-name',
- permissionsEnabled: true,
- setDeviceName: () => Promise.resolve(),
-});
-
+export const Initial = tests.createExample(TestedComponent, {});
diff --git a/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
new file mode 100644
index 000000000..dd1777fd1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/AddNewActionView.tsx
@@ -0,0 +1,79 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { parseTalerUri, TalerUriAction } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { InputWithLabel } from "../components/styled/index.js";
+import { Button } from "../mui/Button.js";
+import { platform } from "../platform/foreground.js";
+
+export interface Props {
+ onCancel: () => Promise<void>;
+}
+
+export function AddNewActionView({ onCancel }: Props): VNode {
+ const [url, setUrl] = useState("");
+ const uri = parseTalerUri(url);
+ const { i18n } = useTranslationContext();
+
+ async function redirectToWallet(): Promise<void> {
+ platform.openWalletURIFromPopup(uri!);
+ }
+
+ return (
+ <Fragment>
+ <section>
+ <InputWithLabel invalid={url !== "" && !uri}>
+ <label>GNU Taler URI</label>
+ <div>
+ <input
+ style={{ width: "100%" }}
+ type="text"
+ value={url}
+ placeholder="taler://pay/...."
+ onInput={(e) => setUrl(e.currentTarget.value)}
+ />
+ </div>
+ </InputWithLabel>
+ </section>
+ <footer>
+ <Button variant="contained" color="secondary" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ {uri && (
+ <Button
+ variant="contained"
+ color="success"
+ onClick={redirectToWallet}
+ >
+ {(() => {
+ switch (uri.type) {
+ case TalerUriAction.Pay:
+ return <i18n.Translate>Open pay page</i18n.Translate>;
+ case TalerUriAction.Refund:
+ return <i18n.Translate>Open refund page</i18n.Translate>;
+ case TalerUriAction.Withdraw:
+ return <i18n.Translate>Open withdraw page</i18n.Translate>;
+ }
+ return <Fragment />;
+ })()}
+ </Button>
+ )}
+ </footer>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx
new file mode 100644
index 000000000..893122c0f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx
@@ -0,0 +1,677 @@
+/*
+ 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/>
+ */
+
+/**
+ * Main entry point for extension pages.
+ *
+ * @author sebasjm
+ */
+
+import {
+ Amounts,
+ TalerUri,
+ TalerUriAction,
+ TranslatedString,
+ parseTalerUri,
+ stringifyTalerUri,
+} from "@gnu-taler/taler-util";
+import {
+ TranslationProvider,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { createHashHistory } from "history";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { Route, Router, route } from "preact-router";
+import { useEffect } from "preact/hooks";
+import {
+ Pages,
+ WalletNavBar,
+ WalletNavBarOptions,
+ getPathnameForTalerURI,
+} from "../NavigationBar.js";
+import { AlertView, CurrentAlerts } from "../components/CurrentAlerts.js";
+import { LogoHeader } from "../components/LogoHeader.js";
+import PendingTransactions from "../components/PendingTransactions.js";
+import {
+ LinkPrimary,
+ RedBanner,
+ SubTitle,
+ WalletAction,
+ WalletBox,
+} from "../components/styled/index.js";
+import { AlertProvider } from "../context/alert.js";
+import { IoCProviderForRuntime } from "../context/iocContext.js";
+import { DepositPage as DepositPageCTA } from "../cta/Deposit/index.js";
+import { InvoiceCreatePage } from "../cta/InvoiceCreate/index.js";
+import { InvoicePayPage } from "../cta/InvoicePay/index.js";
+import { PaymentPage } from "../cta/Payment/index.js";
+import { PaymentTemplatePage } from "../cta/PaymentTemplate/index.js";
+import { RecoveryPage } from "../cta/Recovery/index.js";
+import { RefundPage } from "../cta/Refund/index.js";
+import { TransferCreatePage } from "../cta/TransferCreate/index.js";
+import { TransferPickupPage } from "../cta/TransferPickup/index.js";
+import {
+ WithdrawPageFromParams,
+ WithdrawPageFromURI,
+} from "../cta/Withdraw/index.js";
+import { useIsOnline } from "../hooks/useIsOnline.js";
+import { strings } from "../i18n/strings.js";
+import CloseIcon from "../svg/close_24px.inline.svg";
+import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
+import { AddExchange } from "./AddExchange/index.js";
+import { BackupPage } from "./BackupPage.js";
+import { DepositPage } from "./DepositPage/index.js";
+import { DestinationSelectionPage } from "./DestinationSelection/index.js";
+import { DeveloperPage } from "./DeveloperPage.js";
+import { HistoryPage } from "./History.js";
+import { NotificationsPage } from "./Notifications/index.js";
+import { ProviderDetailPage } from "./ProviderDetailPage.js";
+import { QrReaderPage } from "./QrReader.js";
+import { SettingsPage } from "./Settings.js";
+import { TransactionPage } from "./Transaction.js";
+import { WelcomePage } from "./Welcome.js";
+import { 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();
+ const hash_history = createHashHistory();
+
+ async function redirectToTxInfo(tid: string): Promise<void> {
+ redirectTo(Pages.balanceTransaction({ tid }));
+ }
+ function redirectToURL(str: string): void {
+ window.location.href = new URL(str).href
+ }
+
+ return (
+ <TranslationProvider source={strings}>
+ <IoCProviderForRuntime>
+ <Router history={hash_history}>
+ <Route
+ path={Pages.welcome}
+ component={() => (
+ <WalletTemplate goToURL={redirectToURL}>
+ <WelcomePage />
+ </WalletTemplate>
+ )}
+ />
+
+ <Route
+ path={Pages.qr}
+ component={() => (
+ <WalletTemplate goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <QrReaderPage
+ onDetected={(talerActionUrl: TalerUri) => {
+ redirectTo(
+ Pages.defaultCta({
+ uri: stringifyTalerUri(talerActionUrl),
+ }),
+ );
+ }}
+ />
+ </WalletTemplate>
+ )}
+ />
+
+ <Route
+ path={Pages.settings}
+ component={() => (
+ <WalletTemplate goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <SettingsPage />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.notifications}
+ component={() => (
+ <WalletTemplate goToURL={redirectToURL}>
+ <NotificationsPage />
+ </WalletTemplate>
+ )}
+ />
+ {/**
+ * SETTINGS
+ */}
+ <Route
+ path={Pages.settingsExchangeAdd.pattern}
+ component={() => (
+ <WalletTemplate goToURL={redirectToURL}>
+ <AddExchange onBack={() => redirectTo(Pages.balance)} />
+ </WalletTemplate>
+ )}
+ />
+
+ <Route
+ path={Pages.balanceHistory.pattern}
+ component={({ currency }: { currency?: string }) => (
+ <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <HistoryPage
+ currency={currency}
+ goToWalletDeposit={(currency: string) =>
+ redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ }
+ goToWalletManualWithdraw={(currency?: string) =>
+ redirectTo(
+ Pages.receiveCash({
+ amount: !currency ? undefined : `${currency}:0`,
+ }),
+ )
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.searchHistory.pattern}
+ component={({ currency }: { currency?: string }) => (
+ <WalletTemplate path="balance" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <HistoryPage
+ currency={currency}
+ search
+ goToWalletDeposit={(currency: string) =>
+ redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+ }
+ goToWalletManualWithdraw={(currency?: string) =>
+ redirectTo(
+ Pages.receiveCash({
+ amount: !currency ? undefined : `${currency}:0`,
+ }),
+ )
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.sendCash.pattern}
+ component={({ amount }: { amount?: string }) => (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <DestinationSelectionPage
+ type="send"
+ amount={amount}
+ goToWalletBankDeposit={(amount: string) =>
+ redirectTo(Pages.balanceDeposit({ amount }))
+ }
+ goToWalletWalletSend={(amount: string) =>
+ redirectTo(Pages.ctaTransferCreate({ amount }))
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.receiveCash.pattern}
+ component={({ amount }: { amount?: string }) => (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <DestinationSelectionPage
+ type="get"
+ amount={amount}
+ goToWalletManualWithdraw={(amount?: string) =>
+ redirectTo(Pages.ctaWithdrawManual({ amount }))
+ }
+ goToWalletWalletInvoice={(amount?: string) =>
+ redirectTo(Pages.ctaInvoiceCreate({ amount }))
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
+
+ <Route
+ path={Pages.balanceTransaction.pattern}
+ component={({ tid }: { tid: string }) => (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <TransactionPage
+ tid={tid}
+ goToWalletHistory={(currency?: string) =>
+ redirectTo(Pages.balanceHistory({ currency }))
+ }
+ />
+ </WalletTemplate>
+ )}
+ />
+
+ <Route
+ path={Pages.balanceDeposit.pattern}
+ component={({ amount }: { amount: string }) => (
+ <WalletTemplate path="balance" goToURL={redirectToURL}>
+ <DepositPage
+ amount={amount}
+ onCancel={(currency: string) => {
+ redirectTo(Pages.balanceHistory({ currency }));
+ }}
+ onSuccess={(currency: string) => {
+ redirectTo(Pages.balanceHistory({ currency }));
+ }}
+ />
+ </WalletTemplate>
+ )}
+ />
+
+ <Route
+ path={Pages.backup}
+ component={() => (
+ <WalletTemplate path="backup" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <BackupPage
+ onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.backupProviderDetail.pattern}
+ component={({ pid }: { pid: string }) => (
+ <WalletTemplate goToURL={redirectToURL}>
+ <ProviderDetailPage
+ pid={pid}
+ onPayProvider={(uri: string) =>
+ redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+ }
+ onWithdraw={(amount: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
+ onBack={() => redirectTo(Pages.backup)}
+ />
+ </WalletTemplate>
+ )}
+ />
+ <Route
+ path={Pages.backupProviderAdd}
+ component={() => (
+ <WalletTemplate goToURL={redirectToURL}>
+ <AddBackupProviderPage
+ onPaymentRequired={(uri: string) =>
+ redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+ }
+ onComplete={(pid: string) =>
+ redirectTo(Pages.backupProviderDetail({ pid }))
+ }
+ onBack={() => redirectTo(Pages.backup)}
+ />
+ </WalletTemplate>
+ )}
+ />
+
+ {/**
+ * DEV
+ */}
+ <Route
+ path={Pages.dev}
+ component={() => (
+ <WalletTemplate path="dev" goToTransaction={redirectToTxInfo} goToURL={redirectToURL}>
+ <DeveloperPage />
+ </WalletTemplate>
+ )}
+ />
+
+ {/**
+ * CALL TO ACTION
+ */}
+ <Route
+ path={Pages.defaultCta.pattern}
+ component={({ uri }: { uri: string }) => {
+ const path = getPathnameForTalerURI(uri);
+ if (!path) {
+ return (
+ <CallToActionTemplate title={i18n.str`Taler URI handler`}>
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`Could not found a handler for the Taler URI`,
+ description: i18n.str`The uri read in the path parameter is not valid: "${uri}"`,
+ }}
+ />
+ </CallToActionTemplate>
+ );
+ }
+ return <Redirect to={path} />;
+ }}
+ />
+ <Route
+ path={Pages.ctaPay}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash payment`}>
+ <PaymentPage
+ talerPayUri={decodeURIComponent(talerUri)}
+ goToWalletManualWithdraw={(amount?: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaPayTemplate}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash payment`}>
+ <PaymentTemplatePage
+ talerTemplateUri={decodeURIComponent(talerUri)}
+ goToWalletManualWithdraw={(amount?: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaRefund}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash refund`}>
+ <RefundPage
+ talerRefundUri={decodeURIComponent(talerUri)}
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaWithdraw}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
+ <WithdrawPageFromURI
+ talerWithdrawUri={decodeURIComponent(talerUri)}
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaWithdrawManual.pattern}
+ component={({
+ amount,
+ talerUri,
+ }: {
+ amount: string;
+ talerUri: string;
+ }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
+ <WithdrawPageFromParams
+ onAmountChanged={async (newamount) => {
+ const page = `${Pages.ctaWithdrawManual({ amount: newamount })}?talerUri=${encodeURIComponent(talerUri)}`;
+ redirectTo(page);
+ }}
+ talerExchangeWithdrawUri={talerUri}
+ amount={amount}
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaDeposit}
+ component={({
+ amount,
+ talerUri,
+ }: {
+ amount: string;
+ talerUri: string;
+ }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash deposit`}>
+ <DepositPageCTA
+ amountStr={Amounts.stringify(Amounts.parseOrThrow(amount))}
+ talerDepositUri={decodeURIComponent(talerUri)}
+ cancel={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaInvoiceCreate.pattern}
+ component={({ amount }: { amount: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash invoice`}>
+ <InvoiceCreatePage
+ amount={Amounts.stringify(Amounts.parseOrThrow(amount))}
+ onClose={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaTransferCreate.pattern}
+ component={({ amount }: { amount: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash transfer`}>
+ <TransferCreatePage
+ amount={Amounts.stringify(Amounts.parseOrThrow(amount))}
+ onClose={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaInvoicePay}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash invoice`}>
+ <InvoicePayPage
+ talerPayPullUri={decodeURIComponent(talerUri)}
+ goToWalletManualWithdraw={(amount?: string) =>
+ redirectTo(Pages.receiveCash({ amount }))
+ }
+ onClose={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaTransferPickup}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash transfer`}>
+ <TransferPickupPage
+ talerPayPushUri={decodeURIComponent(talerUri)}
+ onClose={() => redirectTo(Pages.balance)}
+ onSuccess={(tid: string) =>
+ redirectTo(Pages.balanceTransaction({ tid }))
+ }
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaRecovery}
+ component={({ talerRecoveryUri }: { talerRecoveryUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Digital cash recovery`}>
+ <RecoveryPage
+ talerRecoveryUri={decodeURIComponent(talerRecoveryUri)}
+ onCancel={() => redirectTo(Pages.balance)}
+ onSuccess={() => redirectTo(Pages.backup)}
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaExperiment}
+ component={({ talerUri }: { talerUri: string }) => (
+ <CallToActionTemplate title={i18n.str`Development experiment`}>
+ <DevExperimentPage
+ talerExperimentUri={decodeURIComponent(talerUri)}
+ onCancel={() => redirectTo(Pages.balanceHistory({}))}
+ onSuccess={() => redirectTo(Pages.balanceHistory({}))}
+ />
+ </CallToActionTemplate>
+ )}
+ />
+ <Route
+ path={Pages.ctaAddExchange}
+ component={({ talerUri }: { talerUri: string }) => {
+ const tUri = parseTalerUri(decodeURIComponent(talerUri))
+ const baseUrl = tUri?.type === TalerUriAction.AddExchange ? tUri.exchangeBaseUrl : undefined
+ if (!baseUrl) {
+ redirectTo(Pages.balanceHistory({}))
+ return <div>
+ invalid url {talerUri}
+ </div>
+ }
+ return <CallToActionTemplate title={i18n.str`Add exchange`}>
+ <ConfirmAddExchangeView
+ url={baseUrl}
+ status="confirm"
+ error={undefined}
+ onCancel={() => redirectTo(Pages.balanceHistory({}))}
+ onConfirm={() => redirectTo(Pages.balanceHistory({}))}
+ />
+ </CallToActionTemplate>
+ }}
+ />
+ {/**
+ * NOT FOUND
+ * all redirects should be at the end
+ */}
+ <Route
+ path={Pages.balance}
+ component={() => <Redirect to={Pages.balanceHistory({})} />}
+ />
+
+ <Route
+ default
+ component={() => <Redirect to={Pages.balanceHistory({})} />}
+ />
+ </Router>
+ <EnabledBySettings name="showWalletActivity">
+ <WalletActivity />
+ </EnabledBySettings>
+ </IoCProviderForRuntime>
+ </TranslationProvider>
+ );
+}
+
+async function redirectTo(location: string): Promise<void> {
+ route(location);
+}
+
+function Redirect({ to }: { to: string }): null {
+ useEffect(() => {
+ route(to, true);
+ });
+ return null;
+}
+
+// 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;
+// }
+
+function CallToActionTemplate({
+ title,
+ children,
+}: {
+ title: TranslatedString;
+ children: ComponentChildren;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <WalletAction>
+ <LogoHeader />
+ <section style={{ display: "flex", justifyContent: "right", margin: 0 }}>
+ <LinkPrimary href={Pages.balance}>
+ <div
+ style={{
+ height: 24,
+ width: 24,
+ marginLeft: 4,
+ marginRight: 4,
+ border: "1px solid black",
+ borderRadius: 12,
+ }}
+ dangerouslySetInnerHTML={{ __html: CloseIcon }}
+ />
+ </LinkPrimary>
+ </section>
+ <SubTitle>{title}</SubTitle>
+ <AlertProvider>
+ <CurrentAlerts />
+ {children}
+ </AlertProvider>
+ <section style={{ display: "flex", justifyContent: "right" }}>
+ <LinkPrimary href={Pages.balance}>
+ <i18n.Translate>Return to wallet</i18n.Translate>
+ </LinkPrimary>
+ </section>
+ </WalletAction>
+ );
+}
+
+function WalletTemplate({
+ path,
+ children,
+ goToTransaction,
+ goToURL,
+}: {
+ path?: WalletNavBarOptions;
+ children: ComponentChildren;
+ goToTransaction?: (id: string) => Promise<void>;
+ goToURL: (url: string) => void;
+}): VNode {
+ const online = useIsOnline();
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ {!online && (
+ <div style={{ display: "flex", justifyContent: "center" }}>
+ <RedBanner>{i18n.str`Network is offline`}</RedBanner>
+ </div>
+ )}
+ <LogoHeader />
+ <WalletNavBar path={path} />
+ <PendingTransactions
+ goToTransaction={goToTransaction}
+ goToURL={goToURL} />
+ <WalletBox>
+ <AlertProvider>
+ <CurrentAlerts />
+ {children}
+ </AlertProvider>
+ </WalletBox>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
index 9a53fefe2..cc7c9af67 100644
--- a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,179 +15,183 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
-import { addDays } from 'date-fns';
-import { BackupView as TestedComponent } from './BackupPage';
-import { createExample } from '../test-utils';
+import {
+ AbsoluteTime,
+ AmountString,
+ ProviderPaymentType,
+ TalerPreciseTimestamp,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { addDays } from "date-fns";
+import {
+ ShowRecoveryInfo,
+ BackupView as TestedComponent,
+} from "./BackupPage.js";
export default {
- title: 'wallet/backup/list',
- component: TestedComponent,
- argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
+ title: "backup",
};
-
-export const LotOfProviders = createExample(TestedComponent, {
- providers: [{
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
- },
- "terms": {
- "annualFee": "ARS:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
+export const LotOfProviders = tests.createExample(TestedComponent, {
+ providers: [
+ {
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp:
+ TalerPreciseTimestamp.fromSeconds(1625063925),
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
+ ],
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
+ },
+ terms: {
+ annualFee: "ARS:1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
},
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": addDays(new Date(), 13).getTime()
- }
+ {
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp:
+ TalerPreciseTimestamp.fromSeconds(1625063925),
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
+ ],
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: AbsoluteTime.fromMilliseconds(
+ addDays(new Date(), 13).getTime(),
+ ),
+ },
+ terms: {
+ annualFee: "ARS:1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
},
- "terms": {
- "annualFee": "ARS:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.Pending,
+ {
+ active: false,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.Pending,
+ talerUri: "taler://",
+ },
+ terms: {
+ annualFee: "KUDOS:0.1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
},
- "terms": {
- "annualFee": "KUDOS:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.InsufficientBalance,
+ {
+ active: false,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.InsufficientBalance,
+ amount: "KUDOS:10" as AmountString,
+ },
+ terms: {
+ annualFee: "KUDOS:0.1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
},
- "terms": {
- "annualFee": "KUDOS:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.TermsChanged,
- newTerms: {
- annualFee: 'USD:2',
- storageLimitInMegabytes: 8,
- supportedProtocolVersion: '2',
- },
- oldTerms: {
- annualFee: 'USD:1',
+ {
+ active: false,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.TermsChanged,
+ newTerms: {
+ annualFee: "USD:2" as AmountString,
+ storageLimitInMegabytes: 8,
+ supportedProtocolVersion: "2",
+ },
+ oldTerms: {
+ annualFee: "USD:1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "1",
+ },
+ paidUntil: AbsoluteTime.never(),
+ },
+ terms: {
+ annualFee: "KUDOS:0.1" as AmountString,
storageLimitInMegabytes: 16,
- supportedProtocolVersion: '1',
-
+ supportedProtocolVersion: "0.0",
},
- paidUntil: {
- t_ms: 'never'
- }
},
- "terms": {
- "annualFee": "KUDOS:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.Unpaid,
+ {
+ active: false,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.Unpaid,
+ },
+ terms: {
+ annualFee: "KUDOS:0.1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
},
- "terms": {
- "annualFee": "KUDOS:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }, {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.Unpaid,
+ {
+ active: false,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.Unpaid,
+ },
+ terms: {
+ annualFee: "KUDOS:0.1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
},
- "terms": {
- "annualFee": "KUDOS:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }]
+ ],
});
-
-export const OneProvider = createExample(TestedComponent, {
- providers: [{
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
+export const OneProvider = tests.createExample(TestedComponent, {
+ providers: [
+ {
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp:
+ TalerPreciseTimestamp.fromSeconds(1625063925),
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
+ ],
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
+ },
+ terms: {
+ annualFee: "ARS:1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
},
- "terms": {
- "annualFee": "ARS:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }]
+ ],
});
-
-export const Empty = createExample(TestedComponent, {
- providers: []
+export const Empty = tests.createExample(TestedComponent, {
+ providers: [],
});
+export const Recovery = tests.createExample(ShowRecoveryInfo, {
+ info: "taler://recovery/ASLDKJASLKDJASD",
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
index 712329bf8..8a3710f69 100644
--- a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
@@ -1,146 +1,356 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
-
-import { i18n, Timestamp } from "@gnu-taler/taler-util";
-import { ProviderInfo, ProviderPaymentStatus } from "@gnu-taler/taler-wallet-core";
-import { differenceInMonths, formatDuration, intervalToDuration } from "date-fns";
-import { Fragment, JSX, VNode, h } from "preact";
import {
- BoldLight, ButtonPrimary, ButtonSuccess, Centered,
- CenteredText, CenteredBoldText, PopupBox, RowBorderGray,
- SmallText, SmallLightText, WalletBox
-} from "../components/styled";
-import { useBackupStatus } from "../hooks/useBackupStatus";
-import { Pages } from "../NavigationBar";
+ AbsoluteTime,
+ ProviderInfo,
+ ProviderPaymentPaid,
+ ProviderPaymentStatus,
+ ProviderPaymentType,
+ stringifyRestoreUri,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ differenceInMonths,
+ formatDuration,
+ intervalToDuration,
+} from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { Pages } from "../NavigationBar.js";
+import { ErrorAlertView } from "../components/CurrentAlerts.js";
+import { Loading } from "../components/Loading.js";
+import { QR } from "../components/QR.js";
+import {
+ BoldLight,
+ Centered,
+ CenteredBoldText,
+ CenteredText,
+ RowBorderGray,
+ SmallLightText,
+ SmallText,
+ WarningBox,
+} from "../components/styled/index.js";
+import { alertFromError } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { Button } from "../mui/Button.js";
interface Props {
- onAddProvider: () => void;
+ onAddProvider: () => Promise<void>;
+}
+
+export function ShowRecoveryInfo({
+ info,
+ onClose,
+}: {
+ info: string;
+ onClose: () => Promise<void>;
+}): VNode {
+ const [display, setDisplay] = useState(false);
+ const [copied, setCopied] = useState(false);
+ async function copyText(): Promise<void> {
+ navigator.clipboard.writeText(info);
+ setCopied(true);
+ }
+ useEffect(() => {
+ if (copied) {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }
+ }, [copied]);
+ return (
+ <Fragment>
+ <h2>Wallet Recovery</h2>
+ <WarningBox>Do not share this QR or URI with anyone</WarningBox>
+ <section>
+ <p>
+ The qr code can be scanned by another wallet to keep synchronized with
+ this wallet.
+ </p>
+ <Button variant="contained" onClick={async () => setDisplay((d) => !d)}>
+ {display ? "Hide" : "Show"} QR code
+ </Button>
+ {display && <QR text={JSON.stringify(info)} />}
+ </section>
+
+ <section>
+ <p>You can also use the string version</p>
+ <Button variant="contained" disabled={copied} onClick={copyText}>
+ Copy recovery URI
+ </Button>
+ </section>
+ <footer>
+ <div></div>
+ <div>
+ <Button variant="contained" onClick={onClose}>
+ Close
+ </Button>
+ </div>
+ </footer>
+ </Fragment>
+ );
}
export function BackupPage({ onAddProvider }: Props): VNode {
- const status = useBackupStatus()
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+ const status = useAsyncAsHook(() =>
+ api.wallet.call(WalletApiOperation.GetBackupInfo, {}),
+ );
+ const [recoveryInfo, setRecoveryInfo] = useState<string>("");
if (!status) {
- return <div>Loading...</div>
+ return <Loading />;
+ }
+ if (status.hasError) {
+ return (
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not load backup providers`,
+ status,
+ )}
+ />
+ );
+ }
+
+ async function getRecoveryInfo(): Promise<void> {
+ const r = await api.wallet.call(
+ WalletApiOperation.ExportBackupRecovery,
+ {},
+ );
+ const str = stringifyRestoreUri({
+ walletRootPriv: r.walletRootPriv,
+ providers: r.providers.map((p) => p.url),
+ });
+ setRecoveryInfo(str);
}
- return <BackupView providers={status.providers} onAddProvider={onAddProvider} onSyncAll={status.sync} />;
+
+ const providers = status.response.providers.sort((a, b) => {
+ if (
+ a.paymentStatus.type === ProviderPaymentType.Paid &&
+ b.paymentStatus.type === ProviderPaymentType.Paid
+ ) {
+ return getStatusPaidOrder(a.paymentStatus, b.paymentStatus);
+ }
+ return (
+ getStatusTypeOrder(a.paymentStatus) - getStatusTypeOrder(b.paymentStatus)
+ );
+ });
+
+ if (recoveryInfo) {
+ return (
+ <ShowRecoveryInfo
+ info={recoveryInfo}
+ onClose={async () => setRecoveryInfo("")}
+ />
+ );
+ }
+
+ return (
+ <BackupView
+ providers={providers}
+ onAddProvider={onAddProvider}
+ onSyncAll={async () =>
+ api.wallet.call(WalletApiOperation.RunBackupCycle, {}).then()
+ }
+ onShowInfo={getRecoveryInfo}
+ />
+ );
}
export interface ViewProps {
- providers: ProviderInfo[],
- onAddProvider: () => void;
+ providers: ProviderInfo[];
+ onAddProvider: () => Promise<void>;
onSyncAll: () => Promise<void>;
+ onShowInfo: () => Promise<void>;
}
-export function BackupView({ providers, onAddProvider, onSyncAll }: ViewProps): VNode {
+export function BackupView({
+ providers,
+ onAddProvider,
+ onSyncAll,
+ onShowInfo,
+}: ViewProps): VNode {
+ const { i18n } = useTranslationContext();
return (
- <WalletBox>
+ <Fragment>
<section>
- {providers.map((provider) => <BackupLayout
- status={provider.paymentStatus}
- timestamp={provider.lastSuccessfulBackupTimestamp}
- id={provider.syncProviderBaseUrl}
- active={provider.active}
- title={provider.name}
- />
+ {providers.map((provider, idx) => (
+ <BackupLayout
+ key={idx}
+ status={provider.paymentStatus}
+ timestamp={
+ provider.lastSuccessfulBackupTimestamp
+ ? AbsoluteTime.fromPreciseTimestamp(
+ provider.lastSuccessfulBackupTimestamp,
+ )
+ : undefined
+ }
+ id={provider.syncProviderBaseUrl}
+ active={provider.active}
+ title={provider.name}
+ />
+ ))}
+ {!providers.length && (
+ <Centered style={{ marginTop: 100 }}>
+ <BoldLight>
+ <i18n.Translate>No backup providers configured</i18n.Translate>
+ </BoldLight>
+ <Button variant="contained" color="success" onClick={onAddProvider}>
+ <i18n.Translate>Add provider</i18n.Translate>
+ </Button>
+ </Centered>
)}
- {!providers.length && <Centered style={{ marginTop: 100 }}>
- <BoldLight>No backup providers configured</BoldLight>
- <ButtonSuccess onClick={onAddProvider}><i18n.Translate>Add provider</i18n.Translate></ButtonSuccess>
- </Centered>}
</section>
- {!!providers.length && <footer>
- <div />
- <div>
- <ButtonPrimary onClick={onSyncAll}>{
- providers.length > 1 ?
- <i18n.Translate>Sync all backups</i18n.Translate> :
- <i18n.Translate>Sync now</i18n.Translate>
- }</ButtonPrimary>
- <ButtonSuccess onClick={onAddProvider}>Add provider</ButtonSuccess>
- </div>
- </footer>}
- </WalletBox>
- )
+ {!!providers.length && (
+ <footer>
+ <div>
+ <Button variant="contained" onClick={onShowInfo}>
+ Show recovery
+ </Button>
+ </div>
+ <div>
+ <Button variant="contained" onClick={onSyncAll}>
+ {providers.length > 1
+ ? i18n.str`Sync all backups`
+ : i18n.str`Sync now`}
+ </Button>
+ <Button variant="contained" color="success" onClick={onAddProvider}>
+ <i18n.Translate>Add provider</i18n.Translate>
+ </Button>
+ </div>
+ </footer>
+ )}
+ </Fragment>
+ );
}
interface TransactionLayoutProps {
status: ProviderPaymentStatus;
- timestamp?: Timestamp;
+ timestamp?: AbsoluteTime;
title: string;
id: string;
active: boolean;
}
-function BackupLayout(props: TransactionLayoutProps): JSX.Element {
+function BackupLayout(props: TransactionLayoutProps): VNode {
+ const { i18n } = useTranslationContext();
const date = !props.timestamp ? undefined : new Date(props.timestamp.t_ms);
const dateStr = date?.toLocaleString([], {
dateStyle: "medium",
timeStyle: "short",
} as any);
-
return (
<RowBorderGray>
<div style={{ color: !props.active ? "grey" : undefined }}>
- <a href={Pages.provider_detail.replace(':pid', encodeURIComponent(props.id))}><span>{props.title}</span></a>
+ <a
+ href={Pages.backupProviderDetail({
+ pid: encodeURIComponent(props.id),
+ })}
+ >
+ <span>{props.title}</span>
+ </a>
- {dateStr && <SmallText style={{ marginTop: 5 }}>Last synced: {dateStr}</SmallText>}
- {!dateStr && <SmallLightText style={{ marginTop: 5 }}>Not synced</SmallLightText>}
+ {dateStr && (
+ <SmallText style={{ marginTop: 5 }}>
+ <i18n.Translate>Last synced</i18n.Translate>: {dateStr}
+ </SmallText>
+ )}
+ {!dateStr && (
+ <SmallLightText style={{ marginTop: 5 }}>
+ <i18n.Translate>Not synced</i18n.Translate>
+ </SmallLightText>
+ )}
</div>
<div>
- {props.status?.type === 'paid' ?
- <ExpirationText until={props.status.paidUntil} /> :
+ {props.status?.type === "paid" ? (
+ <ExpirationText until={props.status.paidUntil} />
+ ) : (
<div>{props.status.type}</div>
- }
+ )}
</div>
</RowBorderGray>
);
}
-function ExpirationText({ until }: { until: Timestamp }) {
- return <Fragment>
- <CenteredText> Expires in </CenteredText>
- <CenteredBoldText {...({ color: colorByTimeToExpire(until) })}> {daysUntil(until)} </CenteredBoldText>
- </Fragment>
+function ExpirationText({ until }: { until: AbsoluteTime }): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <CenteredText>
+ <i18n.Translate>Expires in</i18n.Translate>
+ </CenteredText>
+ <CenteredBoldText {...{ color: colorByTimeToExpire(until) }}>
+ {" "}
+ {daysUntil(until)}{" "}
+ </CenteredBoldText>
+ </Fragment>
+ );
}
-function colorByTimeToExpire(d: Timestamp) {
- if (d.t_ms === 'never') return 'rgb(28, 184, 65)'
- const months = differenceInMonths(d.t_ms, new Date())
- return months > 1 ? 'rgb(28, 184, 65)' : 'rgb(223, 117, 20)';
+function colorByTimeToExpire(d: AbsoluteTime): string {
+ if (d.t_ms === "never") return "rgb(28, 184, 65)";
+ const months = differenceInMonths(d.t_ms, new Date());
+ return months > 1 ? "rgb(28, 184, 65)" : "rgb(223, 117, 20)";
}
-function daysUntil(d: Timestamp) {
- if (d.t_ms === 'never') return undefined
+function daysUntil(d: AbsoluteTime): string {
+ if (d.t_ms === "never") return "";
const duration = intervalToDuration({
start: d.t_ms,
end: new Date(),
- })
+ });
const str = formatDuration(duration, {
- delimiter: ', ',
+ delimiter: ", ",
format: [
- duration?.years ? 'years' : (
- duration?.months ? 'months' : (
- duration?.days ? 'days' : (
- duration.hours ? 'hours' : 'minutes'
- )
- )
- )
- ]
- })
- return `${str}`
-} \ No newline at end of file
+ duration?.years
+ ? "years"
+ : duration?.months
+ ? "months"
+ : duration?.days
+ ? "days"
+ : duration.hours
+ ? "hours"
+ : "minutes",
+ ],
+ });
+ return `${str}`;
+}
+
+function getStatusTypeOrder(t: ProviderPaymentStatus): number {
+ return [
+ ProviderPaymentType.InsufficientBalance,
+ ProviderPaymentType.TermsChanged,
+ ProviderPaymentType.Unpaid,
+ ProviderPaymentType.Paid,
+ ProviderPaymentType.Pending,
+ ].indexOf(t.type);
+}
+
+function getStatusPaidOrder(
+ a: ProviderPaymentPaid,
+ b: ProviderPaymentPaid,
+): number {
+ return a.paidUntil.t_ms === "never"
+ ? -1
+ : b.paidUntil.t_ms === "never"
+ ? 1
+ : a.paidUntil.t_ms - b.paidUntil.t_ms;
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx
deleted file mode 100644
index cccda203e..000000000
--- a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, NullLink } from '../test-utils';
-import { BalanceView as TestedComponent } from './BalancePage';
-
-export default {
- title: 'wallet/balance',
- component: TestedComponent,
- argTypes: {
- }
-};
-
-
-export const NotYetLoaded = createExample(TestedComponent, {
-});
-
-export const GotError = createExample(TestedComponent, {
- balance: {
- hasError: true,
- message: 'Network error'
- },
- Linker: NullLink,
-});
-
-export const EmptyBalance = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: []
- },
- },
- Linker: NullLink,
-});
-
-export const SomeCoins = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: [{
- available: 'USD:10.5',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- }]
- },
- },
- Linker: NullLink,
-});
-
-export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: [{
- available: 'USD:2.23',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:5.11',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- }]
- },
- },
- Linker: NullLink,
-});
-
-export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, {
- balance: {
- hasError: false,
- response: {
- balances: [{
- available: 'USD:2',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:5',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- },{
- available: 'EUR:4',
- hasPendingTransactions: false,
- pendingIncoming: 'EUR:5',
- pendingOutgoing: 'EUR:0',
- requiresUserInput: false
- }]
- },
- },
- Linker: NullLink,
-});
diff --git a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
deleted file mode 100644
index eb5a0447c..000000000
--- a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- amountFractionalBase, Amounts,
- Balance, BalancesResponse,
- i18n
-} from "@gnu-taler/taler-util";
-import { JSX } from "preact";
-import { ButtonPrimary, Centered, WalletBox } from "../components/styled/index";
-import { BalancesHook, useBalances } from "../hooks/useBalances";
-import { PageLink, renderAmount } from "../renderHtml";
-
-
-export function BalancePage({ goToWalletManualWithdraw }: { goToWalletManualWithdraw: () => void }) {
- const balance = useBalances()
- return <BalanceView balance={balance} Linker={PageLink} goToWalletManualWithdraw={goToWalletManualWithdraw} />
-}
-
-export interface BalanceViewProps {
- balance: BalancesHook;
- Linker: typeof PageLink;
- goToWalletManualWithdraw: () => void;
-}
-
-export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) {
- if (!balance) {
- return <span />
- }
-
- if (balance.hasError) {
- return (
- <div>
- <p>{i18n.str`Error: could not retrieve balance information.`}</p>
- <p>
- Click <Linker pageName="welcome">here</Linker> for help and
- diagnostics.
- </p>
- </div>
- )
- }
- if (balance.response.balances.length === 0) {
- return (
- <p><i18n.Translate>
- You have no balance to show. Need some{" "}
- <Linker pageName="/welcome">help</Linker> getting started?
- </i18n.Translate></p>
- )
- }
- return <ShowBalances wallet={balance.response}
- onWithdraw={goToWalletManualWithdraw}
- />
-}
-
-function formatPending(entry: Balance): JSX.Element {
- let incoming: JSX.Element | undefined;
- let payment: JSX.Element | undefined;
-
- const available = Amounts.parseOrThrow(entry.available);
- const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
- const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
-
- if (!Amounts.isZero(pendingIncoming)) {
- incoming = (
- <span><i18n.Translate>
- <span style={{ color: "darkgreen" }}>
- {"+"}
- {renderAmount(entry.pendingIncoming)}
- </span>{" "}
- incoming
- </i18n.Translate></span>
- );
- }
-
- const l = [incoming, payment].filter((x) => x !== undefined);
- if (l.length === 0) {
- return <span />;
- }
-
- if (l.length === 1) {
- return <span>({l})</span>;
- }
- return (
- <span>
- ({l[0]}, {l[1]})
- </span>
- );
-}
-
-
-function ShowBalances({ wallet, onWithdraw }: { wallet: BalancesResponse, onWithdraw: () => void }) {
- return <WalletBox>
- <section>
- <Centered>{wallet.balances.map((entry) => {
- const av = Amounts.parseOrThrow(entry.available);
- const v = av.value + av.fraction / amountFractionalBase;
- return (
- <p key={av.currency}>
- <span>
- <span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "}
- <span>{av.currency}</span>
- </span>
- {formatPending(entry)}
- </p>
- );
- })}</Centered>
- </section>
- <footer>
- <div />
- <ButtonPrimary onClick={onWithdraw} >Withdraw</ButtonPrimary>
- </footer>
- </WalletBox>
-}
diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx
deleted file mode 100644
index 35da52392..000000000
--- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample } from '../test-utils';
-import { CreateManualWithdraw as TestedComponent } from './CreateManualWithdraw';
-
-export default {
- title: 'wallet/manual withdraw/creation',
- component: TestedComponent,
- argTypes: {
- }
-};
-
-
-export const InitialState = createExample(TestedComponent, {
-});
-
-export const WithExchangeFilled = createExample(TestedComponent, {
- currency: 'COL',
- initialExchange: 'http://exchange.taler:8081',
-});
-
-export const WithExchangeAndAmountFilled = createExample(TestedComponent, {
- currency: 'COL',
- initialExchange: 'http://exchange.taler:8081',
- initialAmount: '10'
-});
-
-export const WithExchangeError = createExample(TestedComponent, {
- initialExchange: 'http://exchange.tal',
- error: 'The exchange url seems invalid'
-});
-
-export const WithAmountError = createExample(TestedComponent, {
- currency: 'COL',
- initialExchange: 'http://exchange.taler:8081',
- initialAmount: 'e'
-});
diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
deleted file mode 100644
index be2cbe41d..000000000
--- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { VNode } from "preact";
-import { useEffect, useRef, useState } from "preact/hooks";
-import { ErrorMessage } from "../components/ErrorMessage";
-import { ButtonPrimary, Input, InputWithLabel, LightText, WalletBox } from "../components/styled";
-
-export interface Props {
- error: string | undefined;
- currency: string | undefined;
- initialExchange?: string;
- initialAmount?: string;
- onExchangeChange: (exchange: string) => void;
- onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>;
-}
-
-export function CreateManualWithdraw({ onExchangeChange, initialExchange, initialAmount, error, currency, onCreate }: Props): VNode {
- const [exchange, setExchange] = useState(initialExchange || "");
- const [amount, setAmount] = useState(initialAmount || "");
- const parsedAmount = Amounts.parse(`${currency}:${amount}`)
-
- let timeout = useRef<number | undefined>(undefined);
- useEffect(() => {
- if (timeout) window.clearTimeout(timeout.current)
- timeout.current = window.setTimeout(async () => {
- onExchangeChange(exchange)
- }, 1000);
- }, [exchange])
-
-
- return (
- <WalletBox>
- <section>
- <ErrorMessage title={error && "Can't create the reserve"} description={error} />
- <h2>Manual Withdrawal</h2>
- <LightText>Choose a exchange to create a reserve and then fill the reserve to withdraw the coins</LightText>
- <p>
- <Input invalid={!!exchange && !currency}>
- <label>Exchange</label>
- <input type="text" placeholder="https://" value={exchange} onChange={(e) => setExchange(e.currentTarget.value)} />
- <small>http://exchange.taler:8081</small>
- </Input>
- {currency && <InputWithLabel invalid={!!amount && !parsedAmount}>
- <label>Amount</label>
- <div>
- <div>{currency}</div>
- <input type="number" style={{ paddingLeft: `${currency.length}em` }} value={amount} onChange={e => setAmount(e.currentTarget.value)} />
- </div>
- </InputWithLabel>}
- </p>
- </section>
- <footer>
- <div />
- <ButtonPrimary disabled={!parsedAmount || !exchange} onClick={() => onCreate(exchange, parsedAmount!)}>Create</ButtonPrimary>
- </footer>
- </WalletBox>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
new file mode 100644
index 000000000..838739ad1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/index.ts
@@ -0,0 +1,121 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { AmountJson, PaytoUri } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import {
+ AmountFieldHandler,
+ ButtonHandler,
+ SelectFieldHandler,
+} from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { ManageAccountPage } from "../ManageAccount/index.js";
+import { useComponentState } from "./state.js";
+import {
+ AmountOrCurrencyErrorView,
+ NoAccountToDepositView,
+ NoEnoughBalanceView,
+ ReadyView,
+} from "./views.js";
+
+export interface Props {
+ amount?: string;
+ onCancel: (currency: string) => void;
+ onSuccess: (currency: string) => void;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | State.AmountOrCurrencyError
+ | State.NoEnoughBalance
+ | State.Ready
+ | State.NoAccounts
+ | State.AddingAccount;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface AddingAccount {
+ status: "manage-account";
+ error: undefined;
+ currency: string;
+ onAccountAdded: (p: string) => void;
+ onCancel: () => void;
+ }
+
+ export interface AmountOrCurrencyError {
+ status: "amount-or-currency-error";
+ error: undefined;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+
+ export interface NoEnoughBalance extends BaseInfo {
+ status: "no-enough-balance";
+ currency: string;
+ }
+
+ export interface NoAccounts extends BaseInfo {
+ status: "no-accounts";
+ currency: string;
+ onAddAccount: ButtonHandler;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ currency: string;
+
+ currentAccount: PaytoUri;
+ totalFee: AmountJson;
+ totalToDeposit: AmountJson;
+
+ amount: AmountFieldHandler;
+ account: SelectFieldHandler;
+ cancelHandler: ButtonHandler;
+ depositHandler: ButtonHandler;
+ onAddAccount: ButtonHandler;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ "amount-or-currency-error": AmountOrCurrencyErrorView,
+ "no-enough-balance": NoEnoughBalanceView,
+ "no-accounts": NoAccountToDepositView,
+ "manage-account": ManageAccountPage,
+ ready: ReadyView,
+};
+
+export const DepositPage = compose(
+ "DepositPage",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
new file mode 100644
index 000000000..97b2ab517
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts
@@ -0,0 +1,276 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AmountJson,
+ Amounts,
+ DepositGroupFees,
+ KnownBankAccountsInfo,
+ parsePaytoUri,
+ PaytoUri,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { RecursiveState } from "../../utils/index.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ amount: amountStr,
+ onCancel,
+ onSuccess,
+}: Props): RecursiveState<State> {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const { pushAlertOnError } = useAlertContext();
+ const parsed = amountStr === undefined ? undefined : Amounts.parse(amountStr);
+ const currency = parsed !== undefined ? parsed.currency : undefined;
+
+ const hook = useAsyncAsHook(async () => {
+ const { balances } = await api.wallet.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ const { accounts } = await api.wallet.call(
+ WalletApiOperation.ListKnownBankAccounts,
+ { currency },
+ );
+
+ return { accounts, balances };
+ });
+
+ const initialValue =
+ parsed !== undefined
+ ? parsed
+ : currency !== undefined
+ ? Amounts.zeroOfCurrency(currency)
+ : undefined;
+ // const [accountIdx, setAccountIdx] = useState<number>(0);
+ const [selectedAccount, setSelectedAccount] = useState<PaytoUri>();
+
+ const [addingAccount, setAddingAccount] = useState(false);
+
+ if (!currency) {
+ return {
+ status: "amount-or-currency-error",
+ error: undefined,
+ };
+ }
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(i18n,
+ i18n.str`Could not load balance information`, hook),
+ };
+ }
+ const { accounts, balances } = hook.response;
+
+ async function updateAccountFromList(accountStr: string): Promise<void> {
+ const uri = !accountStr ? undefined : parsePaytoUri(accountStr);
+ if (uri) {
+ setSelectedAccount(uri);
+ }
+ }
+
+ if (addingAccount) {
+ return {
+ status: "manage-account",
+ error: undefined,
+ currency,
+ onAccountAdded: (p: string) => {
+ updateAccountFromList(p);
+ setAddingAccount(false);
+ hook.retry();
+ },
+ onCancel: () => {
+ setAddingAccount(false);
+ hook.retry();
+ },
+ };
+ }
+
+ const bs = balances.filter((b) => b.available.startsWith(currency));
+ const balance =
+ bs.length > 0
+ ? Amounts.parseOrThrow(bs[0].available)
+ : Amounts.zeroOfCurrency(currency);
+
+ if (Amounts.isZero(balance)) {
+ return {
+ status: "no-enough-balance",
+ error: undefined,
+ currency,
+ };
+ }
+
+ if (accounts.length === 0) {
+ return {
+ status: "no-accounts",
+ error: undefined,
+ currency,
+ onAddAccount: {
+ onClick: pushAlertOnError(async () => {
+ setAddingAccount(true);
+ }),
+ },
+ };
+ }
+ const firstAccount = accounts[0].uri;
+ const currentAccount = !selectedAccount ? firstAccount : selectedAccount;
+
+ return () => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [amount, setAmount] = useState<AmountJson>(
+ initialValue ?? ({} as any),
+ );
+ const amountStr = Amounts.stringify(amount);
+ const depositPaytoUri = stringifyPaytoUri(currentAccount);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const hook = useAsyncAsHook(async () => {
+ const fee = await api.wallet.call(WalletApiOperation.PrepareDeposit, {
+ amount: amountStr,
+ depositPaytoUri,
+ });
+
+ return { fee };
+ }, [amountStr, depositPaytoUri]);
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load fee for amount ${amountStr}`,
+ hook,
+ ),
+ };
+ }
+
+ const { fee } = hook.response;
+
+ const accountMap = createLabelsForBankAccount(accounts);
+
+ const totalFee =
+ fee !== undefined
+ ? Amounts.sum([fee.fees.wire, fee.fees.coin, fee.fees.refresh]).amount
+ : Amounts.zeroOfCurrency(currency);
+
+ const totalToDeposit =
+ fee !== undefined
+ ? Amounts.sub(amount, totalFee).amount
+ : Amounts.zeroOfCurrency(currency);
+
+ const isDirty = amount !== initialValue;
+ const amountError = !isDirty
+ ? undefined
+ : Amounts.cmp(balance, amount) === -1
+ ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
+ : undefined;
+
+ const unableToDeposit =
+ Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee
+ fee === undefined || //no fee calculated yet
+ amountError !== undefined; //amount field may be invalid
+
+ async function doSend(): Promise<void> {
+ if (!currency) return;
+
+ const depositPaytoUri = stringifyPaytoUri(currentAccount);
+ const amountStr = Amounts.stringify(amount);
+ await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
+ amount: amountStr,
+ depositPaytoUri,
+ });
+ onSuccess(currency);
+ }
+
+ return {
+ status: "ready",
+ error: undefined,
+ currency,
+ amount: {
+ value: amount,
+ onInput: pushAlertOnError(async (a) => setAmount(a)),
+ error: amountError,
+ },
+ onAddAccount: {
+ onClick: pushAlertOnError(async () => {
+ setAddingAccount(true);
+ }),
+ },
+ account: {
+ list: accountMap,
+ value: stringifyPaytoUri(currentAccount),
+ onChange: pushAlertOnError(updateAccountFromList),
+ },
+ currentAccount,
+ cancelHandler: {
+ onClick: pushAlertOnError(async () => {
+ onCancel(currency);
+ }),
+ },
+ depositHandler: {
+ onClick: unableToDeposit ? undefined : pushAlertOnError(doSend),
+ },
+ totalFee,
+ totalToDeposit,
+ };
+ };
+}
+
+export function labelForAccountType(id: string): string {
+ switch (id) {
+ case "":
+ return "Choose one";
+ case "x-taler-bank":
+ return "Taler Bank";
+ case "bitcoin":
+ return "Bitcoin";
+ case "iban":
+ return "IBAN";
+ default:
+ return id;
+ }
+}
+
+export function createLabelsForBankAccount(
+ knownBankAccounts: Array<KnownBankAccountsInfo>,
+): { [value: string]: string } {
+ const initialList: Record<string, string> = {};
+ if (!knownBankAccounts.length) return initialList;
+ return knownBankAccounts.reduce((prev, cur, i) => {
+ prev[stringifyPaytoUri(cur.uri)] = cur.alias;
+ return prev;
+ }, initialList);
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
new file mode 100644
index 000000000..c23f83fdd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/stories.tsx
@@ -0,0 +1,116 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "deposit",
+};
+
+export const WithNoAccountForIBAN = tests.createExample(ReadyView, {
+ status: "ready",
+ account: {
+ list: {},
+ value: "",
+ onChange: nullFunction,
+ },
+ currentAccount: {
+ isKnown: true,
+ targetType: "iban",
+ iban: "ABCD1234",
+ params: {},
+ targetPath: "/ABCD1234",
+ },
+ currency: "USD",
+ amount: {
+ onInput: nullFunction,
+ value: Amounts.parseOrThrow("USD:10"),
+ },
+ onAddAccount: {},
+ cancelHandler: {},
+ depositHandler: {
+ onClick: nullFunction,
+ },
+ totalFee: Amounts.zeroOfCurrency("USD"),
+ totalToDeposit: Amounts.parseOrThrow("USD:10"),
+ // onCalculateFee: alwaysReturnFeeToOne,
+});
+
+export const WithIBANAccountTypeSelected = tests.createExample(ReadyView, {
+ status: "ready",
+ account: {
+ list: { asdlkajsdlk: "asdlkajsdlk", qwerqwer: "qwerqwer" },
+ value: "asdlkajsdlk",
+ onChange: nullFunction,
+ },
+ currentAccount: {
+ isKnown: true,
+ targetType: "iban",
+ iban: "ABCD1234",
+ params: {},
+ targetPath: "/ABCD1234",
+ },
+ currency: "USD",
+ amount: {
+ onInput: nullFunction,
+ value: Amounts.parseOrThrow("USD:10"),
+ },
+ onAddAccount: {},
+ cancelHandler: {},
+ depositHandler: {
+ onClick: nullFunction,
+ },
+ totalFee: Amounts.zeroOfCurrency("USD"),
+ totalToDeposit: Amounts.parseOrThrow("USD:10"),
+ // onCalculateFee: alwaysReturnFeeToOne,
+});
+
+export const NewBitcoinAccountTypeSelected = tests.createExample(ReadyView, {
+ status: "ready",
+ account: {
+ list: {},
+ value: "asdlkajsdlk",
+ onChange: nullFunction,
+ },
+ currentAccount: {
+ isKnown: true,
+ targetType: "iban",
+ iban: "ABCD1234",
+ params: {},
+ targetPath: "/ABCD1234",
+ },
+ onAddAccount: {},
+ currency: "USD",
+ amount: {
+ onInput: nullFunction,
+ value: Amounts.parseOrThrow("USD:10"),
+ },
+ cancelHandler: {},
+ depositHandler: {
+ onClick: nullFunction,
+ },
+ totalFee: Amounts.zeroOfCurrency("USD"),
+ totalToDeposit: Amounts.parseOrThrow("USD:10"),
+ // onCalculateFee: alwaysReturnFeeToOne,
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
new file mode 100644
index 000000000..157cb868a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts
@@ -0,0 +1,431 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ Amounts,
+ AmountString,
+ DepositGroupFees,
+ parsePaytoUri,
+ PrepareDepositResponse,
+ ScopeType,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { expect } from "chai";
+import * as tests from "@gnu-taler/web-util/testing";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
+
+import { useComponentState } from "./state.js";
+
+const currency = "EUR";
+const amount = `${currency}:0`;
+const withoutFee = (): PrepareDepositResponse => ({
+ effectiveDepositAmount: `${currency}:5` as AmountString,
+ totalDepositCost: `${currency}:5` as AmountString,
+ fees: {
+ coin: Amounts.stringify(`${currency}:0`),
+ wire: Amounts.stringify(`${currency}:0`),
+ refresh: Amounts.stringify(`${currency}:0`),
+ },
+});
+
+const withSomeFee = (): PrepareDepositResponse => ({
+ effectiveDepositAmount: `${currency}:5` as AmountString,
+ totalDepositCost: `${currency}:5` as AmountString,
+ fees: {
+ coin: Amounts.stringify(`${currency}:1`),
+ wire: Amounts.stringify(`${currency}:1`),
+ refresh: Amounts.stringify(`${currency}:1`),
+ },
+});
+
+describe("DepositPage states", () => {
+ it("should have status 'no-enough-balance' when balance is empty", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
+
+ handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
+ balances: [
+ {
+ flags: [],
+ available: `${currency}:0` as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: `${currency}:0` as AmountString,
+ pendingOutgoing: `${currency}:0` as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency,
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ ],
+ });
+ handler.addWalletCallResponse(
+ WalletApiOperation.ListKnownBankAccounts,
+ undefined,
+ {
+ accounts: [],
+ },
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ ({ status }) => {
+ expect(status).equal("no-enough-balance");
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should have status 'no-accounts' when balance is not empty and accounts is empty", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
+
+ handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
+ balances: [
+ {
+ flags: [],
+ available: `${currency}:1` as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: `${currency}:0` as AmountString,
+ pendingOutgoing: `${currency}:0` as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency,
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ ],
+ });
+ handler.addWalletCallResponse(
+ WalletApiOperation.ListKnownBankAccounts,
+ undefined,
+ {
+ accounts: [],
+ },
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ ({ status }) => {
+ expect(status).equal("no-accounts");
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ const ibanPayto = {
+ uri: parsePaytoUri("payto://iban/ES8877998399652238")!,
+ kyc_completed: false,
+ currency: "EUR",
+ alias: "my iban account",
+ };
+ const talerBankPayto = {
+ uri: parsePaytoUri("payto://x-taler-bank/ES8877998399652238")!,
+ kyc_completed: false,
+ currency: "EUR",
+ alias: "my taler account",
+ };
+
+ it("should have status 'ready' but unable to deposit ", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
+
+ handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
+ balances: [
+ {
+ flags: [],
+ available: `${currency}:1` as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: `${currency}:0` as AmountString,
+ pendingOutgoing: `${currency}:0` as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency,
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ ],
+ });
+ handler.addWalletCallResponse(
+ WalletApiOperation.ListKnownBankAccounts,
+ undefined,
+ {
+ accounts: [ibanPayto],
+ },
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.PrepareDeposit,
+ undefined,
+ withoutFee(),
+ );
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(stringifyPaytoUri(ibanPayto.uri));
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.depositHandler.onClick).undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should not be able to deposit more than the balance ", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
+
+ handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
+ balances: [
+ {
+ flags: [],
+ available: `${currency}:5` as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: `${currency}:0` as AmountString,
+ pendingOutgoing: `${currency}:0` as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency,
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ ],
+ });
+ handler.addWalletCallResponse(
+ WalletApiOperation.ListKnownBankAccounts,
+ undefined,
+ {
+ accounts: [talerBankPayto, ibanPayto],
+ },
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.PrepareDeposit,
+ undefined,
+ withoutFee(),
+ );
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.PrepareDeposit,
+ undefined,
+ withoutFee(),
+ );
+
+ const accountSelected = stringifyPaytoUri(ibanPayto.uri);
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.depositHandler.onClick).undefined;
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
+ expect(state.account.onChange).not.undefined;
+
+ state.account.onChange!(accountSelected);
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(accountSelected);
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
+ expect(state.depositHandler.onClick).undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(accountSelected);
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
+ expect(state.depositHandler.onClick).undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should calculate the fee upon entering amount ", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+ const props = { amount, onCancel: nullFunction, onSuccess: nullFunction };
+
+ handler.addWalletCallResponse(WalletApiOperation.GetBalances, undefined, {
+ balances: [
+ {
+ flags: [],
+ available: `${currency}:10` as AmountString,
+ hasPendingTransactions: false,
+ pendingIncoming: `${currency}:0` as AmountString,
+ pendingOutgoing: `${currency}:0` as AmountString,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency,
+ type: ScopeType.Auditor,
+ url: "asd",
+ },
+ },
+ ],
+ });
+ handler.addWalletCallResponse(
+ WalletApiOperation.ListKnownBankAccounts,
+ undefined,
+ {
+ accounts: [talerBankPayto, ibanPayto],
+ },
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.PrepareDeposit,
+ undefined,
+ withoutFee(),
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.PrepareDeposit,
+ undefined,
+ withSomeFee(),
+ );
+ handler.addWalletCallResponse(
+ WalletApiOperation.PrepareDeposit,
+ undefined,
+ withSomeFee(),
+ );
+
+ const accountSelected = stringifyPaytoUri(ibanPayto.uri);
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(stringifyPaytoUri(talerBankPayto.uri));
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.depositHandler.onClick).undefined;
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
+ expect(state.account.onChange).not.undefined;
+
+ state.account.onChange!(accountSelected);
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(accountSelected);
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:0"));
+ expect(state.depositHandler.onClick).undefined;
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
+
+ expect(state.amount.onInput).not.undefined;
+ if (!state.amount.onInput) return;
+ state.amount.onInput(Amounts.parseOrThrow("EUR:10"));
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(accountSelected);
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
+ expect(state.totalToDeposit).deep.eq(
+ Amounts.parseOrThrow(`${currency}:7`),
+ );
+ expect(state.depositHandler.onClick).not.undefined;
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ expect(state.cancelHandler.onClick).not.undefined;
+ expect(state.currency).eq(currency);
+ expect(state.account.value).eq(accountSelected);
+ expect(state.amount.value).deep.eq(Amounts.parseOrThrow("EUR:10"));
+ expect(state.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
+ expect(state.totalToDeposit).deep.eq(
+ Amounts.parseOrThrow(`${currency}:7`),
+ );
+ expect(state.depositHandler.onClick).not.undefined;
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
new file mode 100644
index 000000000..908becb04
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage/views.tsx
@@ -0,0 +1,191 @@
+/*
+ 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, PaytoUri } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { AmountField } from "../../components/AmountField.js";
+import { ErrorMessage } from "../../components/ErrorMessage.js";
+import { SelectList } from "../../components/SelectList.js";
+import { Input, SubTitle, WarningBox } from "../../components/styled/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Button } from "../../mui/Button.js";
+import { Grid } from "../../mui/Grid.js";
+import { State } from "./index.js";
+
+export function AmountOrCurrencyErrorView(
+ p: State.AmountOrCurrencyError,
+): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <ErrorMessage
+ title={i18n.str`A currency or an amount should be indicated`}
+ />
+ );
+}
+
+export function NoEnoughBalanceView({
+ currency,
+}: State.NoEnoughBalance): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <ErrorMessage
+ title={i18n.str`There is no enough balance to make a deposit for currency ${currency}`}
+ />
+ );
+}
+
+function AccountDetails({ account }: { account: PaytoUri }): VNode {
+ if (account.isKnown) {
+ if (account.targetType === "bitcoin") {
+ return (
+ <dl>
+ <dt>Bitcoin</dt>
+ <dd>{account.targetPath}</dd>
+ </dl>
+ );
+ }
+ if (account.targetType === "x-taler-bank") {
+ return (
+ <dl>
+ <dt>Bank host</dt>
+ <dd>{account.targetPath.split("/")[0]}</dd>
+ <dt>Account name</dt>
+ <dd>{account.targetPath.split("/")[1]}</dd>
+ </dl>
+ );
+ }
+ if (account.targetType === "iban") {
+ return (
+ <dl>
+ <dt>IBAN</dt>
+ <dd>{account.targetPath}</dd>
+ </dl>
+ );
+ }
+ }
+ return <Fragment />;
+}
+
+export function NoAccountToDepositView({
+ currency,
+ onAddAccount,
+}: State.NoAccounts): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <SubTitle>
+ <i18n.Translate>Send {currency} to your account</i18n.Translate>
+ </SubTitle>
+
+ <WarningBox>
+ <i18n.Translate>
+ There is no account to make a deposit for currency {currency}
+ </i18n.Translate>
+ </WarningBox>
+
+ <Button onClick={onAddAccount.onClick} variant="contained">
+ <i18n.Translate>Add account</i18n.Translate>
+ </Button>
+ </Fragment>
+ );
+}
+
+export function ReadyView(state: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <SubTitle>
+ <i18n.Translate>Send {state.currency} to your account</i18n.Translate>
+ </SubTitle>
+ <section>
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-between",
+ marginBottom: 16,
+ }}
+ >
+ <Input>
+ <SelectList
+ label={i18n.str`Select account`}
+ list={state.account.list}
+ name="account"
+ value={state.account.value}
+ onChange={state.account.onChange}
+ />
+ </Input>
+ <Button
+ onClick={state.onAddAccount.onClick}
+ variant="text"
+ style={{ marginLeft: "auto" }}
+ >
+ <i18n.Translate>Manage accounts</i18n.Translate>
+ </Button>
+ </div>
+
+ <p>
+ <AccountDetails account={state.currentAccount} />
+ </p>
+ <Grid container spacing={2} columns={1}>
+ <Grid item xs={1}>
+ <AmountField label={i18n.str`Amount`} handler={state.amount} />
+ </Grid>
+ <Grid item xs={1}>
+ <AmountField
+ label={i18n.str`Deposit fee`}
+ handler={{
+ value: state.totalFee,
+ }}
+ />
+ </Grid>
+ <Grid item xs={1}>
+ <AmountField
+ label={i18n.str`Total deposit`}
+ handler={{
+ value: state.totalToDeposit,
+ }}
+ />
+ </Grid>
+ </Grid>
+ </section>
+ <footer>
+ <Button
+ variant="contained"
+ color="secondary"
+ onClick={state.cancelHandler.onClick}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ {!state.depositHandler.onClick ? (
+ <Button variant="contained" disabled>
+ <i18n.Translate>Deposit</i18n.Translate>
+ </Button>
+ ) : (
+ <Button variant="contained" onClick={state.depositHandler.onClick}>
+ <i18n.Translate>
+ Deposit&nbsp;{Amounts.stringifyValue(state.totalToDeposit)}{" "}
+ {state.currency}
+ </i18n.Translate>
+ </Button>
+ )}
+ </footer>
+ </Fragment>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
new file mode 100644
index 000000000..b56fe5523
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts
@@ -0,0 +1,98 @@
+/*
+ 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 {
+ AmountFieldHandler,
+ ButtonHandler,
+ ToggleHandler,
+} from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ReadyView, SelectCurrencyView } from "./views.js";
+
+export type Props = PropsGet | PropsSend;
+
+interface PropsGet {
+ type: "get";
+ amount?: string;
+ goToWalletManualWithdraw: (amount: string) => void;
+ goToWalletWalletInvoice: (amount: string) => void;
+}
+interface PropsSend {
+ type: "send";
+ amount?: string;
+ goToWalletBankDeposit: (amount: string) => void;
+ goToWalletWalletSend: (amount: string) => void;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | State.Ready
+ | State.SelectCurrency;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface SelectCurrency {
+ status: "select-currency";
+ error: undefined;
+ currencies: Record<string, string>;
+ onCurrencySelected: (currency: string) => void;
+ }
+
+ export interface Ready {
+ status: "ready";
+ error: undefined;
+ type: Props["type"];
+ selectCurrency: ButtonHandler;
+ selectMax: ButtonHandler;
+ previous: Contact[];
+ goToBank: ButtonHandler;
+ goToWallet: ButtonHandler;
+ amountHandler: AmountFieldHandler;
+ }
+}
+
+export type Contact = {
+ icon_type: string;
+ name: string;
+ description: string;
+};
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ "select-currency": SelectCurrencyView,
+ ready: ReadyView,
+};
+
+export const DestinationSelectionPage = compose(
+ "DestinationSelectionPage",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
new file mode 100644
index 000000000..d4e270a6c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts
@@ -0,0 +1,198 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Amounts } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { RecursiveState, assertUnreachable } from "../../utils/index.js";
+import { Contact, Props, State } from "./index.js";
+
+export function useComponentState(props: Props): RecursiveState<State> {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+
+ const parsedInitialAmount = !props.amount
+ ? undefined
+ : Amounts.parse(props.amount);
+
+ const hook = useAsyncAsHook(async () => {
+ if (!parsedInitialAmount) return undefined;
+ const balance = await api.wallet.call(WalletApiOperation.GetBalanceDetail, {
+ currency: parsedInitialAmount.currency,
+ });
+ return { balance };
+ });
+
+ const info = hook && !hook.hasError ? hook.response : undefined;
+
+ // const initialCurrency = parsedInitialAmount?.currency;
+
+ const [amount, setAmount] = useState(
+ !parsedInitialAmount ? undefined : parsedInitialAmount,
+ );
+ //FIXME: get this information from wallet
+ // eslint-disable-next-line no-constant-condition
+ const previous: Contact[] = true
+ ? []
+ : [
+ {
+ name: "International Bank",
+ icon_type: "bank",
+ description: "account ending with 3454",
+ },
+ {
+ name: "Max",
+ icon_type: "bank",
+ description: "account ending with 3454",
+ },
+ {
+ name: "Alex",
+ icon_type: "bank",
+ description: "account ending with 3454",
+ },
+ ];
+
+ if (!amount) {
+ return () => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { i18n } = useTranslationContext();
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const hook = useAsyncAsHook(() =>
+ api.wallet.call(WalletApiOperation.ListExchanges, {}),
+ );
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(i18n,
+ i18n.str`Could not load exchanges`, hook),
+ };
+ }
+ const currencies: Record<string, string> = {};
+ hook.response.exchanges.forEach((e) => {
+ if (e.currency) {
+ currencies[e.currency] = e.currency;
+ }
+ });
+ currencies[""] = "Select a currency";
+
+ return {
+ status: "select-currency",
+ error: undefined,
+ onCurrencySelected: (c: string) => {
+ setAmount(Amounts.zeroOfCurrency(c));
+ },
+ currencies,
+ };
+ };
+ }
+
+ const currencyAndAmount = Amounts.stringify(amount);
+ const invalid = Amounts.isZero(amount);
+
+ switch (props.type) {
+ case "send":
+ return {
+ status: "ready",
+ error: undefined,
+ previous,
+ selectCurrency: {
+ onClick: pushAlertOnError(async () => {
+ setAmount(undefined);
+ }),
+ },
+ goToBank: {
+ onClick: invalid
+ ? undefined
+ : pushAlertOnError(async () => {
+ props.goToWalletBankDeposit(currencyAndAmount);
+ }),
+ },
+ selectMax: {
+ onClick: pushAlertOnError(async () => {
+ const resp = await api.wallet.call(
+ WalletApiOperation.GetMaxDepositAmount,
+ {
+ currency: amount.currency,
+ },
+ );
+ setAmount(Amounts.parseOrThrow(resp.effectiveAmount));
+ }),
+ },
+ goToWallet: {
+ onClick: invalid
+ ? undefined
+ : pushAlertOnError(async () => {
+ props.goToWalletWalletSend(currencyAndAmount);
+ }),
+ },
+ amountHandler: {
+ onInput: pushAlertOnError(async (s) => setAmount(s)),
+ value: amount,
+ },
+ type: props.type,
+ };
+ case "get":
+ return {
+ status: "ready",
+ error: undefined,
+ previous,
+ selectCurrency: {
+ onClick: pushAlertOnError(async () => {
+ setAmount(undefined);
+ }),
+ },
+ selectMax: {
+ onClick: invalid
+ ? undefined
+ : pushAlertOnError(async () => {
+ props.goToWalletManualWithdraw(currencyAndAmount);
+ }),
+ },
+ goToBank: {
+ onClick: invalid
+ ? undefined
+ : pushAlertOnError(async () => {
+ props.goToWalletManualWithdraw(currencyAndAmount);
+ }),
+ },
+ goToWallet: {
+ onClick: invalid
+ ? undefined
+ : pushAlertOnError(async () => {
+ props.goToWalletWalletInvoice(currencyAndAmount);
+ }),
+ },
+ amountHandler: {
+ onInput: pushAlertOnError(async (s) => setAmount(s)),
+ value: amount,
+ },
+ type: props.type,
+ };
+ default:
+ assertUnreachable(props);
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
new file mode 100644
index 000000000..e1ac958f7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/stories.tsx
@@ -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 { ReadyView, SelectCurrencyView } from "./views.js";
+
+export default {
+ title: "destination",
+};
+
+export const GetCash = tests.createExample(ReadyView, {
+ amountHandler: {
+ value: {
+ currency: "EUR",
+ fraction: 0,
+ value: 2,
+ },
+ },
+ goToBank: {},
+ selectMax: {},
+ goToWallet: {},
+ previous: [],
+ selectCurrency: {},
+ type: "get",
+});
+export const SendCash = tests.createExample(ReadyView, {
+ amountHandler: {
+ value: {
+ currency: "EUR",
+ fraction: 0,
+ value: 1,
+ },
+ },
+ selectMax: {},
+ goToBank: {},
+ goToWallet: {},
+ previous: [],
+ selectCurrency: {},
+ type: "send",
+});
+
+export const SelectCurrency = tests.createExample(SelectCurrencyView, {
+ currencies: {
+ "": "Select a currency",
+ USD: "USD",
+ },
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
new file mode 100644
index 000000000..683378613
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts
@@ -0,0 +1,153 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ Amounts,
+ ExchangeEntryStatus,
+ ExchangeListItem,
+ ExchangeTosStatus,
+ ExchangeUpdateStatus,
+ ScopeType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import * as tests from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { nullFunction } from "../../mui/handlers.js";
+import { createWalletApiMock } from "../../test-utils.js";
+import { useComponentState } from "./state.js";
+
+const exchangeArs: ExchangeListItem = {
+ currency: "ARS",
+ exchangeBaseUrl: "http://",
+ masterPub: "123qwe123",
+ scopeInfo: {
+ currency: "ARS",
+ type: ScopeType.Exchange,
+ url: "http://",
+ },
+ tosStatus: ExchangeTosStatus.Accepted,
+ exchangeEntryStatus: ExchangeEntryStatus.Used,
+ exchangeUpdateStatus: ExchangeUpdateStatus.Initial,
+ paytoUris: [],
+ ageRestrictionOptions: [],
+ lastUpdateTimestamp: undefined,
+ noFees: false,
+ peerPaymentsDisabled: false,
+};
+
+describe("Destination selection states", () => {
+ it("should select currency if no amount specified", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ handler.addWalletCallResponse(
+ WalletApiOperation.ListExchanges,
+ {},
+ {
+ exchanges: [exchangeArs],
+ },
+ );
+
+ const props = {
+ type: "get" as const,
+ goToWalletManualWithdraw: nullFunction,
+ goToWalletWalletInvoice: nullFunction,
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status }) => {
+ expect(status).equal("loading");
+ },
+ (state) => {
+ if (state.status !== "select-currency") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.currencies).deep.eq({
+ ARS: "ARS",
+ "": "Select a currency",
+ });
+
+ state.onCurrencySelected(exchangeArs.currency!);
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.goToBank.onClick).eq(undefined);
+ expect(state.goToWallet.onClick).eq(undefined);
+
+ expect(state.amountHandler.value).deep.eq(
+ Amounts.parseOrThrow("ARS:0"),
+ );
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+
+ it("should be possible to start with an amount specified in request params", async () => {
+ const { handler, TestingContext } = createWalletApiMock();
+
+ const props = {
+ type: "get" as const,
+ goToWalletManualWithdraw: nullFunction,
+ goToWalletWalletInvoice: nullFunction,
+ amount: "ARS:2",
+ };
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ // ({ status }) => {
+ // expect(status).equal("loading");
+ // },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.goToBank.onClick).not.eq(undefined);
+ expect(state.goToWallet.onClick).not.eq(undefined);
+
+ expect(state.amountHandler.value).deep.eq(
+ Amounts.parseOrThrow("ARS:2"),
+ );
+ },
+ (state) => {
+ if (state.status !== "ready") expect.fail();
+ if (state.error) expect.fail();
+ expect(state.goToBank.onClick).not.eq(undefined);
+ expect(state.goToWallet.onClick).not.eq(undefined);
+
+ expect(state.amountHandler.value).deep.eq(
+ Amounts.parseOrThrow("ARS:2"),
+ );
+ },
+ ],
+ TestingContext,
+ );
+
+ expect(hookBehavior).deep.equal({ result: "ok" });
+ expect(handler.getCallingQueueState()).eq("empty");
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
new file mode 100644
index 000000000..8a74a20f1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx
@@ -0,0 +1,430 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { styled } from "@linaria/react";
+import { Fragment, h, VNode } from "preact";
+import { AmountField } from "../../components/AmountField.js";
+import { EnabledBySettings } from "../../components/EnabledBySettings.js";
+import { SelectList } from "../../components/SelectList.js";
+import {
+ Input,
+ LightText,
+ LinkPrimary,
+ SvgIcon,
+} from "../../components/styled/index.js";
+import { Button } from "../../mui/Button.js";
+import { Grid } from "../../mui/Grid.js";
+import { Paper } from "../../mui/Paper.js";
+import { Pages } from "../../NavigationBar.js";
+import arrowIcon from "../../svg/chevron-down.inline.svg";
+import bankIcon from "../../svg/ri-bank-line.inline.svg";
+import { assertUnreachable } from "../../utils/index.js";
+import { Contact, State } from "./index.js";
+
+export function SelectCurrencyView({
+ currencies,
+ onCurrencySelected,
+}: State.SelectCurrency): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <h2>
+ <i18n.Translate>
+ Choose a currency to proceed or add another exchange
+ </i18n.Translate>
+ </h2>
+
+ <p>
+ <Input>
+ <SelectList
+ label={i18n.str`Known currencies`}
+ list={currencies}
+ name="lang"
+ value={""}
+ onChange={(v) => onCurrencySelected(v)}
+ />
+ </Input>
+ </p>
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <div />
+ <LinkPrimary href={Pages.settingsExchangeAdd({})}>
+ <i18n.Translate>Add an exchange</i18n.Translate>
+ </LinkPrimary>
+ </div>
+ </Fragment>
+ );
+}
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ & > * {
+ margin: 8px;
+ }
+`;
+
+const ContactTable = styled.table`
+ width: 100%;
+ & > tr > td {
+ padding: 8px;
+ & > div:not([data-disabled]):hover {
+ background-color: lightblue;
+ }
+ color: black;
+ div[data-disabled] > * {
+ color: gray;
+ }
+ }
+
+ & > tr:nth-child(2n) {
+ background: #ebebeb;
+ }
+`;
+
+const MediaExample = styled.div`
+ text-size-adjust: 100%;
+ color: inherit;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ text-transform: none;
+ text-align: left;
+ box-sizing: border-box;
+ align-items: center;
+ display: flex;
+ padding: 8px 8px;
+
+ &[data-disabled]:hover {
+ cursor: inherit;
+ }
+ cursor: pointer;
+`;
+
+const MediaLeft = styled.div`
+ text-size-adjust: 100%;
+
+ color: inherit;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ text-transform: none;
+ text-align: left;
+ box-sizing: border-box;
+ padding-right: 8px;
+ display: block;
+`;
+
+const MediaBody = styled.div`
+ text-size-adjust: 100%;
+
+ font-family: inherit;
+ text-transform: none;
+ text-align: left;
+ box-sizing: border-box;
+ flex: 1 1;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 1.42857;
+`;
+const MediaRight = styled.div`
+ text-size-adjust: 100%;
+
+ color: inherit;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ text-transform: none;
+ text-align: left;
+ box-sizing: border-box;
+ padding-left: 8px;
+`;
+
+const CircleDiv = styled.div`
+ box-sizing: border-box;
+ align-items: center;
+ background-position: 50%;
+ background-repeat: no-repeat;
+ background-size: cover;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ margin-left: auto;
+ margin-right: auto;
+ overflow: hidden;
+ text-align: center;
+ text-decoration: none;
+ text-transform: uppercase;
+ transition:
+ background-color 0.15s ease,
+ border-color 0.15s ease,
+ color 0.15s ease;
+ font-size: 16px;
+ background-color: #86a7bd1a;
+ height: 40px;
+ line-height: 40px;
+ width: 40px;
+ border: none;
+`;
+
+export function ReadyView(props: State.Ready): VNode {
+ switch (props.type) {
+ case "get":
+ return ReadyGetView(props);
+ case "send":
+ return ReadySendView(props);
+ default:
+ assertUnreachable(props.type);
+ }
+}
+export function ReadyGetView({
+ amountHandler,
+ goToBank,
+ goToWallet,
+ selectCurrency,
+ previous,
+}: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Container>
+ <h1>
+ <i18n.Translate>Specify the amount and the origin</i18n.Translate>
+ </h1>
+ <Grid container columns={2} justifyContent="space-between">
+ <AmountField
+ label={i18n.str`Amount`}
+ required
+ handler={amountHandler}
+ />
+
+ <Button onClick={selectCurrency.onClick}>
+ <i18n.Translate>Change currency</i18n.Translate>
+ </Button>
+ </Grid>
+
+ <Grid container spacing={1} columns={1}>
+ {previous.length > 0 ? (
+ <Fragment>
+ <p>
+ <i18n.Translate>Use previous origins:</i18n.Translate>
+ </p>
+ <Grid item xs={1}>
+ <Paper style={{ padding: 8 }}>
+ <ContactTable>
+ {previous.map((info, i) => (
+ <tr key={i}>
+ <td>
+ <RowExample
+ info={info}
+ disabled={!amountHandler.onInput}
+ />
+ </td>
+ </tr>
+ ))}
+ </ContactTable>
+ </Paper>
+ </Grid>
+ </Fragment>
+ ) : undefined}
+ {previous.length > 0 ? (
+ <Grid item>
+ <p>
+ <i18n.Translate>
+ Or specify the origin of the money
+ </i18n.Translate>
+ </p>
+ </Grid>
+ ) : (
+ <Grid item>
+ <p>
+ <i18n.Translate>Specify the origin of the money</i18n.Translate>
+ </p>
+ </Grid>
+ )}
+ <Grid item container columns={2} spacing={1}>
+ <Grid item xs={1}>
+ <Paper style={{ padding: 8 }}>
+ <p>
+ <i18n.Translate>From my bank account</i18n.Translate>
+ </p>
+ <Button onClick={goToBank.onClick}>
+ <i18n.Translate>Withdraw</i18n.Translate>
+ </Button>
+ </Paper>
+ </Grid>
+ <Grid item xs={1}>
+ <Paper style={{ padding: 8 }}>
+ <p>
+ <i18n.Translate>From another wallet</i18n.Translate>
+ </p>
+ <Button onClick={goToWallet.onClick}>
+ <i18n.Translate>Invoice</i18n.Translate>
+ </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>
+ );
+}
+export function ReadySendView({
+ amountHandler,
+ goToBank,
+ goToWallet,
+ previous,
+ selectMax,
+}: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Container>
+ <h1>
+ <i18n.Translate>Specify the amount and the destination</i18n.Translate>
+ </h1>
+
+ <Grid container columns={2} justifyContent="space-between">
+ <AmountField
+ label={i18n.str`Amount`}
+ required
+ handler={amountHandler}
+ />
+ <EnabledBySettings name="advancedMode">
+ <Button onClick={selectMax.onClick}>
+ <i18n.Translate>Send all</i18n.Translate>
+ </Button>
+ </EnabledBySettings>
+ </Grid>
+
+ <Grid container spacing={1} columns={1}>
+ {previous.length > 0 ? (
+ <Fragment>
+ <p>
+ <i18n.Translate>Use previous destinations:</i18n.Translate>
+ </p>
+ <Grid item xs={1}>
+ <Paper style={{ padding: 8 }}>
+ <ContactTable>
+ {previous.map((info, i) => (
+ <tr key={i}>
+ <td>
+ <RowExample
+ info={info}
+ disabled={!amountHandler.onInput}
+ />
+ </td>
+ </tr>
+ ))}
+ </ContactTable>
+ </Paper>
+ </Grid>
+ </Fragment>
+ ) : undefined}
+ {previous.length > 0 ? (
+ <Grid item>
+ <p>
+ <i18n.Translate>
+ Or specify the destination of the money
+ </i18n.Translate>
+ </p>
+ </Grid>
+ ) : (
+ <Grid item>
+ <p>
+ <i18n.Translate>
+ Specify the destination of the money
+ </i18n.Translate>
+ </p>
+ </Grid>
+ )}
+ <Grid item container columns={2} spacing={1}>
+ <Grid item xs={1}>
+ <Paper style={{ padding: 8 }}>
+ <p>
+ <i18n.Translate>To my bank account</i18n.Translate>
+ </p>
+ <Button onClick={goToBank.onClick}>
+ <i18n.Translate>Deposit</i18n.Translate>
+ </Button>
+ </Paper>
+ </Grid>
+ <Grid item xs={1}>
+ <Paper style={{ padding: 8 }}>
+ <p>
+ <i18n.Translate>To another wallet</i18n.Translate>
+ </p>
+ <Button onClick={goToWallet.onClick}>
+ <i18n.Translate>Send</i18n.Translate>
+ </Button>
+ </Paper>
+ </Grid>
+ </Grid>
+ </Grid>
+ </Container>
+ );
+}
+
+function RowExample({
+ info,
+ disabled,
+}: {
+ info: Contact;
+ disabled?: boolean;
+}): VNode {
+ const icon = info.icon_type === "bank" ? bankIcon : undefined;
+ return (
+ <MediaExample data-disabled={disabled}>
+ <MediaLeft>
+ <CircleDiv>
+ {icon !== undefined ? (
+ <SvgIcon
+ title={info.name}
+ dangerouslySetInnerHTML={{
+ __html: icon,
+ }}
+ color="currentColor"
+ />
+ ) : (
+ <span>A</span>
+ )}
+ </CircleDiv>
+ </MediaLeft>
+ <MediaBody>
+ <span>{info.name}</span>
+ <LightText>{info.description}</LightText>
+ </MediaBody>
+ <MediaRight>
+ <SvgIcon
+ title="Select this contact"
+ dangerouslySetInnerHTML={{ __html: arrowIcon }}
+ color="currentColor"
+ transform="rotate(-90deg)"
+ />
+ </MediaRight>
+ </MediaExample>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
new file mode 100644
index 000000000..e7c9111fd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.stories.tsx
@@ -0,0 +1,50 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { DeveloperPage as TestedComponent } from "./DeveloperPage.js";
+
+export default {
+ title: "developer",
+ component: TestedComponent,
+ argTypes: {
+ setDeviceName: () => Promise.resolve(),
+ },
+};
+
+export const AllOff = tests.createExample(TestedComponent, {
+ onDownloadDatabase: async () => "this is the content of the database",
+ operations: [
+ {
+ id: " ",
+ type: "exchange-update",
+ exchangeBaseUrl: "http://exchange.url.",
+ givesLifeness: false,
+ lastError: undefined,
+ timestampDue: AbsoluteTime.fromMilliseconds(123123213),
+ retryInfo: undefined,
+ isDue: false,
+ isLongpolling: false,
+ },
+ ],
+ coins: [],
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
new file mode 100644
index 000000000..7b6ac8895
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -0,0 +1,695 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ Amounts,
+ CoinDumpJson,
+ CoinStatus,
+ ExchangeTosStatus,
+ LogLevel,
+ NotificationType,
+ ScopeType,
+ stringifyWithdrawExchange,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { Pages } from "../NavigationBar.js";
+import { Checkbox } from "../components/Checkbox.js";
+import { SelectList } from "../components/SelectList.js";
+import { Time } from "../components/Time.js";
+import { ActiveTasksTable } from "../components/WalletActivity.js";
+import {
+ DestructiveText,
+ LinkPrimary,
+ NotifyUpdateFadeOut,
+ SubTitle,
+ SuccessText,
+ WarningText,
+} from "../components/styled/index.js";
+import { useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
+import { Button } from "../mui/Button.js";
+import { Grid } from "../mui/Grid.js";
+import { Paper } from "../mui/Paper.js";
+import { TextField } from "../mui/TextField.js";
+
+type CoinsInfo = CoinDumpJson["coins"];
+type CalculatedCoinfInfo = {
+ // ageKeysCount: number | undefined;
+ denom_value: number;
+ denom_fraction: number;
+ //remain_value: number;
+ status: string;
+ from_refresh: boolean;
+ id: string;
+};
+
+type SplitedCoinInfo = {
+ spent: CalculatedCoinfInfo[];
+ usable: CalculatedCoinfInfo[];
+};
+
+export interface Props {
+ // FIXME: Pending operations don't exist anymore.
+}
+
+function hashObjectId(o: any): string {
+ return JSON.stringify(o);
+}
+
+export function DeveloperPage({}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [downloadedDatabase, setDownloadedDatabase] = useState<
+ { time: Date; content: string } | undefined
+ >(undefined);
+ async function onExportDatabase(): Promise<void> {
+ const db = await api.wallet.call(WalletApiOperation.ExportDb, {});
+ const content = JSON.stringify(db);
+ setDownloadedDatabase({
+ time: new Date(),
+ content,
+ });
+ }
+ const api = useBackendContext();
+
+ const fileRef = useRef<HTMLInputElement>(null);
+ async function onImportDatabase(str: string): Promise<void> {
+ await api.wallet.call(WalletApiOperation.ImportDb, {
+ dump: JSON.parse(str),
+ });
+ }
+ const [settings, updateSettings] = useSettings();
+ const { safely } = useAlertContext();
+
+ const listenAllEvents = Array.from<NotificationType>({ length: 1 });
+ // listenAllEvents.includes = () => true
+
+ const hook = useAsyncAsHook(async () => {
+ const list = await api.wallet.call(WalletApiOperation.ListExchanges, {});
+ const version = await api.wallet.call(WalletApiOperation.GetVersion, {});
+ const coins = await api.wallet.call(WalletApiOperation.DumpCoins, {});
+ return { exchanges: list.exchanges, version, coins };
+ });
+ const exchangeList = hook && !hook.hasError ? hook.response.exchanges : [];
+ const coins = hook && !hook.hasError ? hook.response.coins.coins : [];
+
+ useEffect(() => {
+ return api.listener.onUpdateNotification(listenAllEvents, (ev) => {
+ console.log("event", ev);
+ return hook?.retry();
+ });
+ });
+
+ const currencies: { [ex: string]: string } = {};
+ const money_by_exchange = coins.reduce(
+ (prev, cur) => {
+ const denom = Amounts.parseOrThrow(cur.denom_value);
+ if (!prev[cur.exchange_base_url]) {
+ prev[cur.exchange_base_url] = [];
+ currencies[cur.exchange_base_url] = denom.currency;
+ }
+ prev[cur.exchange_base_url].push({
+ // ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length,
+ denom_value: denom.value,
+ denom_fraction: denom.fraction,
+ // remain_value: parseFloat(
+ // Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)),
+ // ),
+ status: cur.coin_status,
+ from_refresh: cur.refresh_parent_coin_pub !== undefined,
+ id: cur.coin_pub,
+ });
+ return prev;
+ },
+ {} as {
+ [exchange_name: string]: CalculatedCoinfInfo[];
+ },
+ );
+
+ const [tagName, setTagName] = useState("");
+ const [logLevel, setLogLevel] = useState("info");
+ return (
+ <div>
+ <p>
+ <i18n.Translate>Debug tools</i18n.Translate>:
+ </p>
+ <Grid container justifyContent="space-between" spacing={1} size={4}>
+ <Grid item>
+ <Button
+ variant="contained"
+ onClick={() =>
+ confirmReset(
+ i18n.str`Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?`,
+ () => api.background.call("resetDb", undefined),
+ )
+ }
+ >
+ <i18n.Translate>reset</i18n.Translate>
+ </Button>
+ </Grid>
+ <Grid item>
+ <Button
+ variant="contained"
+ onClick={() =>
+ confirmReset(
+ i18n.str`TESTING: This may delete all your coin, proceed with caution`,
+ () => api.background.call("runGarbageCollector", undefined),
+ )
+ }
+ >
+ <i18n.Translate>run gc</i18n.Translate>
+ </Button>
+ </Grid>
+ <Grid item>
+ <Button
+ variant="contained"
+ onClick={async () => fileRef?.current?.click()}
+ >
+ <i18n.Translate>import database</i18n.Translate>
+ </Button>
+ </Grid>
+ <Grid item>
+ <input
+ ref={fileRef}
+ style={{ display: "none" }}
+ type="file"
+ onChange={async (e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return Promise.reject();
+ }
+ const buf = await f[0].arrayBuffer();
+ const str = new Uint8Array(buf).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ );
+ return onImportDatabase(str);
+ }}
+ />
+ <Button variant="contained" onClick={onExportDatabase}>
+ <i18n.Translate>export database</i18n.Translate>
+ </Button>
+ </Grid>
+ <Grid item>
+ <Button
+ variant="contained"
+ onClick={async () => {
+ const result = await Promise.all(
+ exchangeList.map(async (exchange) => {
+ const url = exchange.exchangeBaseUrl;
+ const oldKeys = JSON.stringify(
+ await (await fetch(`${url}keys`)).json(),
+ );
+ const newKeys = JSON.stringify(
+ await (
+ await fetch(`${url}keys`, { cache: "no-cache" })
+ ).json(),
+ );
+ return oldKeys !== newKeys;
+ }),
+ );
+ const ex = exchangeList.filter((e, i) => result[i]);
+ if (!ex.length) {
+ alert("no exchange was outdated");
+ } else {
+ alert(`found some exchange out of date: ${result.join(", ")}`);
+ }
+ }}
+ >
+ <i18n.Translate>Clear exchange key cache</i18n.Translate>
+ </Button>
+ </Grid>{" "}
+ </Grid>
+ {downloadedDatabase && (
+ <div>
+ <i18n.Translate>
+ Database exported at{" "}
+ <Time
+ timestamp={AbsoluteTime.fromMilliseconds(
+ downloadedDatabase.time.getTime(),
+ )}
+ format="yyyy/MM/dd HH:mm:ss"
+ />{" "}
+ <a
+ href={`data:text/plain;charset=utf-8;base64,${toBase64(
+ downloadedDatabase.content,
+ )}`}
+ download={`taler-wallet-database-${format(
+ downloadedDatabase.time,
+ "yyyy/MM/dd_HH:mm",
+ )}.json`}
+ >
+ <i18n.Translate>click here</i18n.Translate>
+ </a>{" "}
+ to download
+ </i18n.Translate>
+ </div>
+ )}
+ <Checkbox
+ label={i18n.str`Inject Taler support in all pages`}
+ name="inject"
+ description={
+ <i18n.Translate>
+ Enabling this option will make `window.taler` be available in all
+ sites
+ </i18n.Translate>
+ }
+ enabled={settings.injectTalerSupport!}
+ onToggle={safely("update support injection", async () => {
+ updateSettings("injectTalerSupport", !settings.injectTalerSupport);
+ })}
+ />
+
+ <SubTitle>
+ <i18n.Translate>Exchange Entries</i18n.Translate>
+ </SubTitle>
+ {!exchangeList || !exchangeList.length ? (
+ <div>
+ <i18n.Translate>No exchange yet</i18n.Translate>
+ </div>
+ ) : (
+ <Fragment>
+ <table>
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Currency</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>URL</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Status</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Terms of Service</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Last Update</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Actions</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {exchangeList.map((e, idx) => {
+ function TosStatus(): VNode {
+ switch (e.tosStatus) {
+ case ExchangeTosStatus.Accepted:
+ return (
+ <SuccessText>
+ <i18n.Translate>ok</i18n.Translate>
+ </SuccessText>
+ );
+ case ExchangeTosStatus.Pending:
+ return (
+ <WarningText>
+ <i18n.Translate>pending</i18n.Translate>
+ </WarningText>
+ );
+ case ExchangeTosStatus.Proposed:
+ return <i18n.Translate>proposed</i18n.Translate>;
+ default:
+ return (
+ <DestructiveText>
+ <i18n.Translate>
+ unknown (exchange status should be updated)
+ </i18n.Translate>
+ </DestructiveText>
+ );
+ }
+ }
+ const uri = !e.masterPub
+ ? undefined
+ : stringifyWithdrawExchange({
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ });
+ return (
+ <tr key={idx}>
+ <td>
+ <a href={!uri ? undefined : Pages.defaultCta({ uri })}>
+ {e.scopeInfo
+ ? `${e.scopeInfo.currency} (${
+ e.scopeInfo.type === ScopeType.Global
+ ? "global"
+ : "regional"
+ })`
+ : e.currency}
+ </a>
+ </td>
+ <td>
+ <a
+ href={new URL(`/keys`, e.exchangeBaseUrl).href}
+ target="_blank"
+ >
+ {e.exchangeBaseUrl}
+ </a>
+ </td>
+ <td>
+ {e.exchangeEntryStatus} / {e.exchangeUpdateStatus}
+ </td>
+ <td>
+ <TosStatus />
+ </td>
+ <td>
+ {e.lastUpdateTimestamp
+ ? AbsoluteTime.toIsoString(
+ AbsoluteTime.fromPreciseTimestamp(
+ e.lastUpdateTimestamp,
+ ),
+ )
+ : "never"}
+ </td>
+ <td>
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.UpdateExchangeEntry,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ force: true,
+ },
+ );
+ }}
+ >
+ Reload
+ </button>
+ <button
+ onClick={() => {
+ api.wallet.call(WalletApiOperation.DeleteExchange, {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ });
+ }}
+ >
+ Delete
+ </button>
+ <button
+ onClick={() => {
+ api.wallet.call(WalletApiOperation.DeleteExchange, {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ purge: true,
+ });
+ }}
+ >
+ Purge
+ </button>
+ {e.scopeInfo && e.masterPub && e.currency ? (
+ e.scopeInfo.type === ScopeType.Global ? (
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.RemoveGlobalCurrencyExchange,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ currency: e.currency!,
+ exchangeMasterPub: e.masterPub!,
+ },
+ );
+ }}
+ >
+ Make regional
+ </button>
+ ) : e.scopeInfo.type ===
+ ScopeType.Auditor ? undefined : e.scopeInfo.type ===
+ ScopeType.Exchange ? (
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.AddGlobalCurrencyExchange,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ currency: e.currency!,
+ exchangeMasterPub: e.masterPub!,
+ },
+ );
+ }}
+ >
+ Make global
+ </button>
+ ) : undefined
+ ) : undefined}
+ <button
+ onClick={() => {
+ api.wallet.call(
+ WalletApiOperation.SetExchangeTosForgotten,
+ {
+ exchangeBaseUrl: e.exchangeBaseUrl,
+ },
+ );
+ }}
+ >
+ Forget ToS
+ </button>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </Fragment>
+ )}
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <div />
+ <LinkPrimary href={Pages.settingsExchangeAdd({})}>
+ <i18n.Translate>Add an exchange</i18n.Translate>
+ </LinkPrimary>
+ </div>
+
+ <Paper style={{ padding: 10, margin: 10 }}>
+ <h3>Logging</h3>
+ <div>
+ <TextField
+ label="Tag name"
+ placeholder="wallet.ts"
+ variant="filled"
+ // error={subject.error}
+ required
+ value={tagName}
+ onChange={setTagName}
+ />
+ <SelectList
+ label={i18n.str`Log levels`}
+ list={{
+ trace: "TRACE",
+ info: "INFO",
+ error: "ERROR",
+ }}
+ name="logLevel"
+ value={logLevel}
+ onChange={(v) => setLogLevel(v)}
+ />
+ </div>
+ <Button
+ variant="contained"
+ onClick={async () => {
+ api.background.call("setLoggingLevel", {
+ tag: tagName,
+ level: logLevel as LogLevel,
+ });
+ }}
+ >
+ Set log level
+ </Button>
+ </Paper>
+
+ <br />
+ <p>
+ <i18n.Translate>Coins</i18n.Translate>:
+ </p>
+ {Object.keys(money_by_exchange).map((ex, idx) => {
+ const allcoins = money_by_exchange[ex];
+ allcoins.sort((a, b) => {
+ if (b.denom_value !== a.denom_value) {
+ return b.denom_value - a.denom_value;
+ }
+ return b.denom_fraction - a.denom_fraction;
+ });
+
+ const coins = allcoins.reduce(
+ (prev, cur) => {
+ if (cur.status === CoinStatus.Fresh) prev.usable.push(cur);
+ if (cur.status === CoinStatus.Dormant) prev.spent.push(cur);
+ return prev;
+ },
+ {
+ spent: [],
+ usable: [],
+ } as SplitedCoinInfo,
+ );
+
+ return (
+ <ShowAllCoins
+ key={idx}
+ coins={coins}
+ ex={ex}
+ currencies={currencies}
+ />
+ );
+ })}
+ <br />
+ <NotifyUpdateFadeOut>
+ <ActiveTasksTable />
+ </NotifyUpdateFadeOut>
+ </div>
+ );
+}
+
+function ShowAllCoins({
+ ex,
+ coins,
+ currencies,
+}: {
+ ex: string;
+ coins: SplitedCoinInfo;
+ currencies: { [ex: string]: string };
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [collapsedSpent, setCollapsedSpent] = useState(true);
+ const [collapsedUnspent, setCollapsedUnspent] = useState(false);
+ const totalUsable = coins.usable.reduce(
+ (prev, cur) =>
+ Amounts.add(prev, {
+ currency: "NONE",
+ fraction: cur.denom_fraction,
+ value: cur.denom_value,
+ }).amount,
+ Amounts.zeroOfCurrency("NONE"),
+ );
+ const totalSpent = coins.spent.reduce(
+ (prev, cur) =>
+ Amounts.add(prev, {
+ currency: "NONE",
+ fraction: cur.denom_fraction,
+ value: cur.denom_value,
+ }).amount,
+ Amounts.zeroOfCurrency("NONE"),
+ );
+ return (
+ <Fragment>
+ <p>
+ <b>{ex}</b>: {Amounts.stringifyValue(totalUsable)} {currencies[ex]}
+ </p>
+ <p>
+ spent: {Amounts.stringifyValue(totalSpent)} {currencies[ex]}
+ </p>
+ <p onClick={() => setCollapsedUnspent(true)}>
+ <b>
+ <i18n.Translate>usable coins</i18n.Translate>
+ </b>
+ </p>
+ {collapsedUnspent ? (
+ <div onClick={() => setCollapsedUnspent(false)}>click to show</div>
+ ) : (
+ <table>
+ <tr>
+ <td>
+ <i18n.Translate>id</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>denom</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>status</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>from refresh?</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>age key count</i18n.Translate>
+ </td>
+ </tr>
+ {coins.usable.map((c, idx) => {
+ return (
+ <tr key={idx}>
+ <td>{c.id.substring(0, 5)}</td>
+ <td>
+ {Amounts.stringifyValue({
+ value: c.denom_value,
+ fraction: c.denom_fraction,
+ currency: "ANY",
+ })}
+ </td>
+ <td>{c.status}</td>
+ <td>{c.from_refresh ? "true" : "false"}</td>
+ {/* <td>{String(c.ageKeysCount)}</td> */}
+ </tr>
+ );
+ })}
+ </table>
+ )}
+ <p onClick={() => setCollapsedSpent(true)}>
+ <i18n.Translate>spent coins</i18n.Translate>
+ </p>
+ {collapsedSpent ? (
+ <div onClick={() => setCollapsedSpent(false)}>
+ <i18n.Translate>click to show</i18n.Translate>
+ </div>
+ ) : (
+ <table>
+ <tr>
+ <td>
+ <i18n.Translate>id</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>denom</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>status</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>from refresh?</i18n.Translate>
+ </td>
+ </tr>
+ {coins.spent.map((c, idx) => {
+ return (
+ <tr key={idx}>
+ <td>{c.id.substring(0, 5)}</td>
+ <td>{c.denom_value}</td>
+ <td>{c.status}</td>
+ <td>{c.from_refresh ? "true" : "false"}</td>
+ </tr>
+ );
+ })}
+ </table>
+ )}
+ </Fragment>
+ );
+}
+
+function toBase64(str: string): string {
+ return btoa(
+ encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
+ return String.fromCharCode(parseInt(p1, 16));
+ }),
+ );
+}
+
+export async function confirmReset(
+ confirmTheResetMessage: string,
+ cb: () => Promise<void>,
+): Promise<void> {
+ if (confirm(confirmTheResetMessage)) {
+ await cb();
+ window.close();
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/index.ts b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/index.ts
new file mode 100644
index 000000000..afbaf1945
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/index.ts
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+
+export interface Props {
+ p: string;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ ready: ReadyView,
+};
+
+export const ComponentName = compose(
+ "ComponentName",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts
new file mode 100644
index 000000000..31a351579
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/state.ts
@@ -0,0 +1,24 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Props, State } from "./index.js";
+
+export function useComponentState({ p }: Props): State {
+ return {
+ status: "ready",
+ error: undefined,
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/stories.tsx b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/stories.tsx
new file mode 100644
index 000000000..628e97c02
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/stories.tsx
@@ -0,0 +1,29 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "example",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/test.ts b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/test.ts
new file mode 100644
index 000000000..eae4d4ca2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/test.ts
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("test description", () => {
+ it("should assert", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/views.tsx b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/views.tsx
new file mode 100644
index 000000000..a98bfef60
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/EmptyComponentExample/views.tsx
@@ -0,0 +1,25 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { h, VNode } from "preact";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { State } from "./index.js";
+
+export function ReadyView({ error }: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+
+ return <div />;
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
new file mode 100644
index 000000000..d711f1ecc
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
@@ -0,0 +1,115 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ DenomOperationMap,
+ ExchangeFullDetails,
+ ExchangeListItem,
+ FeeDescriptionPair,
+} from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { HookError } from "../../hooks/useAsyncAsHook.js";
+import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
+import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import {
+ ComparingView,
+ NoExchangesView,
+ PrivacyContentView,
+ ReadyView,
+ TosContentView,
+} from "./views.js";
+
+export interface Props {
+ list: ExchangeListItem[];
+ initialValue: string;
+ onCancel: () => Promise<void>;
+ onSelection: (exchange: string) => Promise<void>;
+}
+
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | State.Ready
+ | State.Comparing
+ | State.ShowingTos
+ | State.ShowingPrivacy
+ | SelectExchangeState.NoExchangeFound;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ exchanges: SelectFieldHandler;
+ selected: ExchangeFullDetails;
+ error: undefined;
+ onShowTerms: ButtonHandler;
+ onShowPrivacy: ButtonHandler;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ onClose: ButtonHandler;
+ }
+
+ export interface Comparing extends BaseInfo {
+ status: "comparing";
+ coinOperationTimeline: DenomOperationMap<FeeDescriptionPair[]>;
+ wireFeeTimeline: Record<string, FeeDescriptionPair[]>;
+ globalFeeTimeline: FeeDescriptionPair[];
+ missingWireTYpe: string[];
+ newWireType: string[];
+ onReset: ButtonHandler;
+ onSelect: ButtonHandler;
+ }
+ export interface ShowingTos {
+ status: "showing-tos";
+ exchangeUrl: string;
+ onClose: ButtonHandler;
+ }
+ export interface ShowingPrivacy {
+ status: "showing-privacy";
+ exchangeUrl: string;
+ onClose: ButtonHandler;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ comparing: ComparingView,
+ "no-exchange-found": NoExchangesView,
+ "showing-tos": TosContentView,
+ "showing-privacy": PrivacyContentView,
+ ready: ReadyView,
+};
+
+export const ExchangeSelectionPage = compose(
+ "ExchangeSelectionPage",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
new file mode 100644
index 000000000..d70b62de0
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
@@ -0,0 +1,242 @@
+/*
+ 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 { DenomOperationMap, FeeDescription } from "@gnu-taler/taler-util";
+import {
+ WalletApiOperation,
+ createPairTimeline,
+} from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ onCancel,
+ onSelection,
+ list: exchanges,
+ initialValue,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
+ const initialValueIdx = exchanges.findIndex(
+ (e) => e.exchangeBaseUrl === initialValue,
+ );
+ if (initialValueIdx === -1) {
+ throw Error(
+ `wrong usage of ExchangeSelection component, currentExchange '${initialValue}' is not in the list of exchanges`,
+ );
+ }
+ const [value, setValue] = useState(String(initialValueIdx));
+
+ const selectedIdx = parseInt(value, 10);
+ const selectedExchange = exchanges[selectedIdx];
+
+ const comparingExchanges = selectedIdx !== initialValueIdx;
+
+ const initialExchange = comparingExchanges
+ ? exchanges[initialValueIdx]
+ : undefined;
+
+ const hook = useAsyncAsHook(async () => {
+ const selected = await api.wallet.call(
+ WalletApiOperation.GetExchangeDetailedInfo,
+ {
+ exchangeBaseUrl: selectedExchange.exchangeBaseUrl,
+ },
+ );
+
+ const original = !initialExchange
+ ? undefined
+ : await api.wallet.call(WalletApiOperation.GetExchangeDetailedInfo, {
+ exchangeBaseUrl: initialExchange.exchangeBaseUrl,
+ });
+
+ return {
+ exchanges,
+ selected: selected.exchange,
+ original: original?.exchange,
+ };
+ }, [selectedExchange, initialExchange]);
+
+ const [showingTos, setShowingTos] = useState<string | undefined>(undefined);
+ const [showingPrivacy, setShowingPrivacy] = useState<string | undefined>(
+ undefined,
+ );
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load exchange details info`,
+ hook,
+ ),
+ };
+ }
+
+ const { selected, original } = hook.response;
+
+ const exchangeMap = exchanges.reduce(
+ (prev, cur, idx) => ({ ...prev, [String(idx)]: cur.exchangeBaseUrl }),
+ {} as Record<string, string>,
+ );
+
+ if (showingPrivacy) {
+ return {
+ status: "showing-privacy",
+ onClose: {
+ onClick: pushAlertOnError(async () => setShowingPrivacy(undefined)),
+ },
+ exchangeUrl: showingPrivacy,
+ };
+ }
+ if (showingTos) {
+ return {
+ status: "showing-tos",
+ onClose: {
+ onClick: pushAlertOnError(async () => setShowingTos(undefined)),
+ },
+ exchangeUrl: showingTos,
+ };
+ }
+
+ if (!comparingExchanges || !original) {
+ // !original <=> selected == original
+ return {
+ status: "ready",
+ exchanges: {
+ list: exchangeMap,
+ value: value,
+ onChange: pushAlertOnError(async (v) => {
+ setValue(v);
+ }),
+ },
+ error: undefined,
+ onClose: {
+ onClick: pushAlertOnError(onCancel),
+ },
+ selected,
+ onShowPrivacy: {
+ onClick: pushAlertOnError(async () => {
+ setShowingPrivacy(selected.exchangeBaseUrl);
+ }),
+ },
+ onShowTerms: {
+ onClick: pushAlertOnError(async () => {
+ setShowingTos(selected.exchangeBaseUrl);
+ }),
+ },
+ };
+ }
+
+ // this may be expensive, useMemo
+ const coinOperationTimeline: DenomOperationMap<FeeDescription[]> = {
+ deposit: createPairTimeline(
+ selected.denomFees.deposit,
+ original.denomFees.deposit,
+ ),
+ refresh: createPairTimeline(
+ selected.denomFees.refresh,
+ original.denomFees.refresh,
+ ),
+ refund: createPairTimeline(
+ selected.denomFees.refund,
+ original.denomFees.refund,
+ ),
+ withdraw: createPairTimeline(
+ selected.denomFees.withdraw,
+ original.denomFees.withdraw,
+ ),
+ };
+
+ const globalFeeTimeline = createPairTimeline(
+ selected.globalFees,
+ original.globalFees,
+ );
+
+ const allWireType = Object.keys(selected.transferFees).concat(
+ Object.keys(original.transferFees),
+ );
+
+ const wireFeeTimeline: Record<string, FeeDescription[]> = {};
+
+ const missingWireTYpe: string[] = [];
+ const newWireType: string[] = [];
+
+ for (const wire of allWireType) {
+ const selectedWire = selected.transferFees[wire];
+ const originalWire = original.transferFees[wire];
+
+ if (!selectedWire) {
+ newWireType.push(wire);
+ continue;
+ }
+ if (!originalWire) {
+ missingWireTYpe.push(wire);
+ continue;
+ }
+
+ wireFeeTimeline[wire] = createPairTimeline(selectedWire, originalWire);
+ }
+
+ return {
+ status: "comparing",
+ exchanges: {
+ list: exchangeMap,
+ value: value,
+ onChange: pushAlertOnError(async (v) => {
+ setValue(v);
+ }),
+ },
+ error: undefined,
+ onReset: {
+ onClick: pushAlertOnError(async () => {
+ setValue(String(initialValue));
+ }),
+ },
+ onSelect: {
+ onClick: pushAlertOnError(async () => {
+ onSelection(selected.exchangeBaseUrl);
+ }),
+ },
+ onShowPrivacy: {
+ onClick: pushAlertOnError(async () => {
+ setShowingPrivacy(selected.exchangeBaseUrl);
+ }),
+ },
+ onShowTerms: {
+ onClick: pushAlertOnError(async () => {
+ setShowingTos(selected.exchangeBaseUrl);
+ }),
+ },
+ selected,
+ coinOperationTimeline,
+ wireFeeTimeline,
+ globalFeeTimeline,
+ missingWireTYpe,
+ newWireType,
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
new file mode 100644
index 000000000..990e2790f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/stories.tsx
@@ -0,0 +1,563 @@
+/*
+ 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 { ComparingView, ReadyView, NoExchangesView } from "./views.js";
+
+export default {
+ title: "select exchange",
+};
+
+export const NoExchangeFound = tests.createExample(NoExchangesView, {
+ currency: "USD",
+ defaultExchange: "https://exchange.taler.ar",
+});
+
+export const Bitcoin1 = tests.createExample(ReadyView, {
+ exchanges: {
+ list: { "0": "https://exchange.taler.ar" },
+ value: "0",
+ },
+ selected: {
+ currency: "BITCOINBTC",
+ auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ denomFees: timelineExample(),
+ transferFees: {},
+ globalFees: [],
+ } as any,
+ onShowPrivacy: {},
+ onShowTerms: {},
+ onClose: {},
+});
+export const Bitcoin2 = tests.createExample(ReadyView, {
+ exchanges: {
+ list: {
+ "https://exchange.taler.ar": "https://exchange.taler.ar",
+ "https://exchange-btc.taler.ar": "https://exchange-btc.taler.ar",
+ },
+ value: "https://exchange.taler.ar",
+ },
+ selected: {
+ currency: "BITCOINBTC",
+ auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ denomFees: timelineExample(),
+ transferFees: {},
+ globalFees: [],
+ } as any,
+ onShowPrivacy: {},
+ onShowTerms: {},
+ onClose: {},
+});
+
+export const Kudos1 = tests.createExample(ReadyView, {
+ exchanges: {
+ list: {
+ "https://exchange-kudos.taler.ar": "https://exchange-kudos.taler.ar",
+ },
+ value: "https://exchange-kudos.taler.ar",
+ },
+ selected: {
+ currency: "BITCOINBTC",
+ auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ denomFees: timelineExample(),
+ transferFees: {},
+ globalFees: [],
+ } as any,
+ onShowPrivacy: {},
+ onShowTerms: {},
+ onClose: {},
+});
+export const Kudos2 = tests.createExample(ReadyView, {
+ exchanges: {
+ list: {
+ "https://exchange-kudos.taler.ar": "https://exchange-kudos.taler.ar",
+ "https://exchange-kudos2.taler.ar": "https://exchange-kudos2.taler.ar",
+ },
+ value: "https://exchange-kudos.taler.ar",
+ },
+ selected: {
+ currency: "BITCOINBTC",
+ auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ denomFees: timelineExample(),
+ transferFees: {},
+ globalFees: [],
+ } as any,
+ onShowPrivacy: {},
+ onShowTerms: {},
+ onClose: {},
+});
+export const ComparingBitcoin = tests.createExample(ComparingView, {
+ exchanges: {
+ list: { "http://exchange": "http://exchange" },
+ value: "http://exchange",
+ },
+ selected: {
+ currency: "BITCOINBTC",
+ auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ transferFees: {},
+ globalFees: [],
+ } as any,
+ onReset: {},
+ onShowPrivacy: {},
+ onShowTerms: {},
+ onSelect: {},
+ error: undefined,
+ coinOperationTimeline: {
+ deposit: [],
+ refresh: [],
+ refund: [],
+ withdraw: [],
+ },
+ globalFeeTimeline: [],
+ newWireType: [],
+ missingWireTYpe: [],
+ wireFeeTimeline: {},
+});
+export const ComparingKudos = tests.createExample(ComparingView, {
+ exchanges: {
+ list: { "http://exchange": "http://exchange" },
+ value: "http://exchange",
+ },
+ selected: {
+ currency: "KUDOS",
+ auditors: [],
+ exchangeBaseUrl: "https://exchange.taler.ar",
+ transferFees: {},
+ globalFees: [],
+ } as any,
+ onReset: {},
+ onShowPrivacy: {},
+ onShowTerms: {},
+ onSelect: {},
+ error: undefined,
+ coinOperationTimeline: {
+ deposit: [],
+ refresh: [],
+ refund: [],
+ withdraw: [],
+ },
+ globalFeeTimeline: [],
+ newWireType: [],
+ missingWireTYpe: [],
+ wireFeeTimeline: {},
+});
+
+function timelineExample() {
+ return {
+ deposit: [
+ {
+ group: "0.1",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1916386904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "1",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1916386904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "10",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1916386904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "1000",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1916386904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "2",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1916386904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "5",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1916386904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ ],
+ refresh: [
+ {
+ group: "0.1",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "1",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "10",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "1000",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "2",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "5",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ ],
+ refund: [
+ {
+ group: "0.1",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "1",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "10",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "1000",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "2",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "5",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ ],
+ withdraw: [
+ {
+ group: "0.1",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "1",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "10",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "1000",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "2",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ {
+ group: "5",
+ from: {
+ t_ms: 1664098904000,
+ },
+ until: {
+ t_ms: 1758706904000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ ],
+ wad: [
+ {
+ group: "iban",
+ from: {
+ t_ms: 1640995200000,
+ },
+ until: {
+ t_ms: 1798761600000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ ],
+ wire: [
+ {
+ group: "iban",
+ from: {
+ t_ms: 1640995200000,
+ },
+ until: {
+ t_ms: 1798761600000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ ],
+ closing: [
+ {
+ group: "iban",
+ from: {
+ t_ms: 1640995200000,
+ },
+ until: {
+ t_ms: 1798761600000,
+ },
+ fee: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ },
+ ],
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/test.ts
new file mode 100644
index 000000000..3c7235851
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/test.ts
@@ -0,0 +1,23 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { AbsoluteTime, Amounts, DenominationInfo } from "@gnu-taler/taler-util";
+import { expect } from "chai";
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
new file mode 100644
index 000000000..6f67d84b7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
@@ -0,0 +1,931 @@
+/*
+ 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 { FeeDescription, FeeDescriptionPair } from "@gnu-taler/taler-util";
+import { styled } from "@linaria/react";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Amount } from "../../components/Amount.js";
+import { AlertView } from "../../components/CurrentAlerts.js";
+import { ErrorMessage } from "../../components/ErrorMessage.js";
+import { SelectList } from "../../components/SelectList.js";
+import { Input, SvgIcon } from "../../components/styled/index.js";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
+import { Time } from "../../components/Time.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { State as SelectExchangeState } from "../../hooks/useSelectedExchange.js";
+import { Button } from "../../mui/Button.js";
+import arrowDown from "../../svg/chevron-down.inline.svg";
+import { State } from "./index.js";
+
+const ButtonGroup = styled.div`
+ & > button {
+ margin-left: 8px;
+ margin-right: 8px;
+ }
+`;
+const ButtonGroupFooter = styled.div`
+ & {
+ display: flex;
+ justify-content: space-between;
+ }
+ & > button {
+ margin-left: 8px;
+ margin-right: 8px;
+ }
+`;
+
+const FeeDescriptionTable = styled.table`
+ & {
+ margin-bottom: 20px;
+ width: 100%;
+ border-collapse: collapse;
+ }
+ td {
+ padding: 8px;
+ }
+ td.fee {
+ text-align: center;
+ }
+ th.fee {
+ text-align: center;
+ }
+ td.value {
+ text-align: right;
+ width: 15%;
+ white-space: nowrap;
+ }
+ td.icon {
+ width: 24px;
+ }
+ td.icon > div {
+ width: 24px;
+ height: 24px;
+ margin: 0px;
+ }
+ td.expiration {
+ text-align: center;
+ }
+
+ tr[data-main="true"] {
+ background-color: #add8e662;
+ }
+ tr[data-main="true"] > td.value,
+ tr[data-main="true"] > td.expiration,
+ tr[data-main="true"] > td.fee {
+ border-bottom: lightgray solid 1px;
+ }
+ tr[data-hidden="true"] {
+ display: none;
+ }
+ tbody > tr.value[data-hasMore="true"],
+ tbody > tr.value[data-hasMore="true"] > td {
+ cursor: pointer;
+ }
+ th {
+ position: sticky;
+ top: 0;
+ background-color: white;
+ }
+`;
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ & > * {
+ margin-bottom: 20px;
+ }
+`;
+
+export function PrivacyContentView({
+ exchangeUrl,
+ onClose,
+}: State.ShowingPrivacy): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div>
+ <Button variant="outlined" onClick={onClose.onClick}>
+ <i18n.Translate>Close</i18n.Translate>
+ </Button>
+ <div>show privacy terms for {exchangeUrl}</div>
+ </div>
+ );
+}
+
+export function TosContentView({
+ exchangeUrl,
+ onClose,
+}: State.ShowingTos): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div>
+ <Button variant="outlined" onClick={onClose.onClick}>
+ <i18n.Translate>Close</i18n.Translate>
+ </Button>
+ <TermsOfService exchangeUrl={exchangeUrl} readOnly >
+ s
+ </TermsOfService>
+ </div>
+ );
+}
+
+export function NoExchangesView({
+ defaultExchange,
+ currency,
+}: SelectExchangeState.NoExchangeFound): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <p>
+ <AlertView
+ alert={{
+ type: "error",
+ message: i18n.str`There is no exchange available for currency ${currency}`,
+ description: i18n.str`You can add more exchanges from the settings.`,
+ cause: undefined,
+ context: undefined,
+ }}
+ />
+ </p>
+ {defaultExchange && (
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`Exchange ${defaultExchange} is not available`,
+ description: i18n.str`Exchange status can view accessed from the settings.`,
+ }}
+ />
+ )}
+ </Fragment>
+ );
+}
+
+export function ComparingView({
+ exchanges,
+ selected,
+ onReset,
+ onSelect,
+ coinOperationTimeline,
+ globalFeeTimeline,
+ wireFeeTimeline,
+ missingWireTYpe,
+ newWireType,
+ onShowPrivacy,
+ onShowTerms,
+}: State.Comparing): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Container>
+ <h2>
+ <i18n.Translate>Service fee description</i18n.Translate>
+ </h2>
+
+ <section>
+ <div
+ style={{
+ display: "flex",
+ flexWrap: "wrap",
+ alignItems: "center",
+ justifyContent: "space-between",
+ }}
+ >
+ <p>
+ <Input>
+ <SelectList
+ label={
+ <i18n.Translate>
+ Select {selected.currency} exchange
+ </i18n.Translate>
+ }
+ list={exchanges.list}
+ name="lang"
+ value={exchanges.value}
+ onChange={exchanges.onChange}
+ />
+ </Input>
+ </p>
+ <ButtonGroup>
+ <Button variant="outlined" onClick={onReset.onClick}>
+ <i18n.Translate>Reset</i18n.Translate>
+ </Button>
+ <Button variant="contained" onClick={onSelect.onClick}>
+ <i18n.Translate>Use this exchange</i18n.Translate>
+ </Button>
+ </ButtonGroup>
+ </div>
+ </section>
+ <section>
+ <dl>
+ <dt>
+ <i18n.Translate>Auditors</i18n.Translate>
+ </dt>
+ {selected.auditors.length === 0 ? (
+ <dd style={{ color: "red" }}>
+ <i18n.Translate>Doesn&apos;t have auditors</i18n.Translate>
+ </dd>
+ ) : (
+ selected.auditors.map((a) => {
+ <dd>{a.auditor_url}</dd>;
+ })
+ )}
+ </dl>
+ <table>
+ <tr>
+ <td>
+ <i18n.Translate>currency</i18n.Translate>
+ </td>
+ <td>{selected.currency}</td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Coin operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by denomination
+ value and is valid for a period of time. The exchange will charge
+ the indicated amount every time a coin is used in such operation.
+ </i18n.Translate>
+ </p>
+ <p>
+ <i18n.Translate>Deposits</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={coinOperationTimeline.deposit}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ <p>
+ <i18n.Translate>Withdrawals</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={coinOperationTimeline.withdraw}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ <p>
+ <i18n.Translate>Refunds</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={coinOperationTimeline.refund}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>{" "}
+ <p>
+ <i18n.Translate>Refresh</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={coinOperationTimeline.refresh}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>{" "}
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Transfer operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by transfer type
+ and is valid for a period of time. The exchange will charge the
+ indicated amount every time a transfer is made.
+ </i18n.Translate>
+ </p>
+ {missingWireTYpe.map((type) => {
+ return (
+ <p key={type}>
+ Wire <b>{type}</b> is not supported for this exchange.
+ </p>
+ );
+ })}
+ {newWireType.map((type) => {
+ return (
+ <Fragment key={type}>
+ <p>
+ Wire <b>{type}</b> is not supported for the previous exchange.
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Operation</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.transferFees[type]}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </Fragment>
+ );
+ })}
+ {Object.entries(wireFeeTimeline).map(([type, fees], idx) => {
+ return (
+ <Fragment key={idx}>
+ <p>{type}</p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Operation</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={fees}
+ sorting={(a, b) => a.localeCompare(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </Fragment>
+ );
+ })}
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Wallet operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by transfer type
+ and is valid for a period of time. The exchange will charge the
+ indicated amount every time a transfer is made.
+ </i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Feature</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Current</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Selected</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeePairByValue
+ list={globalFeeTimeline}
+ sorting={(a, b) => a.localeCompare(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </section>
+ <section>
+ <ButtonGroupFooter>
+ <Button onClick={onShowPrivacy.onClick} variant="outlined">
+ Privacy policy
+ </Button>
+ <Button onClick={onShowTerms.onClick} variant="outlined">
+ Terms of service
+ </Button>
+ </ButtonGroupFooter>
+ </section>
+ <section>
+ <ButtonGroupFooter>
+ <Button onClick={onShowPrivacy.onClick} variant="outlined">
+ Privacy policy
+ </Button>
+ <Button onClick={onShowTerms.onClick} variant="outlined">
+ Terms of service
+ </Button>
+ </ButtonGroupFooter>
+ </section>
+ </Container>
+ );
+}
+
+export function ReadyView({
+ exchanges,
+ selected,
+ onClose,
+ onShowPrivacy,
+ onShowTerms,
+}: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Container>
+ <h2>
+ <i18n.Translate>Service fee description</i18n.Translate>
+ </h2>
+ <p>
+ All fee indicated below are in the same and only currency the exchange
+ works.
+ </p>
+ <section>
+ <div
+ style={{
+ display: "flex",
+ flexWrap: "wrap",
+ alignItems: "center",
+ justifyContent: "space-between",
+ }}
+ >
+ {Object.keys(exchanges.list).length === 1 ? (
+ <Fragment>
+ <p>Exchange: {selected.exchangeBaseUrl}</p>
+ </Fragment>
+ ) : (
+ <p>
+ <Input>
+ <SelectList
+ label={
+ <i18n.Translate>
+ Select {selected.currency} exchange
+ </i18n.Translate>
+ }
+ list={exchanges.list}
+ name="lang"
+ value={exchanges.value}
+ onChange={exchanges.onChange}
+ />
+ </Input>
+ </p>
+ )}
+ <Button variant="outlined" onClick={onClose.onClick}>
+ <i18n.Translate>Close</i18n.Translate>
+ </Button>
+ </div>
+ </section>
+ <section>
+ <dl>
+ <dt>Auditors</dt>
+ {selected.auditors.length === 0 ? (
+ <dd style={{ color: "red" }}>
+ <i18n.Translate>Doesn&apos;t have auditors</i18n.Translate>
+ </dd>
+ ) : (
+ selected.auditors.map((a) => {
+ <dd>{a.auditor_url}</dd>;
+ })
+ )}
+ </dl>
+ <table>
+ <tr>
+ <td>
+ <i18n.Translate>Currency</i18n.Translate>
+ </td>
+ <td>
+ <b>{selected.currency}</b>
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Coin operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by denomination
+ value and is valid for a period of time. The exchange will charge
+ the indicated amount every time a coin is used in such operation.
+ </i18n.Translate>
+ </p>
+ <p>
+ <i18n.Translate>Deposits</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.deposit}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ <p>
+ <i18n.Translate>Withdrawals</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.withdraw}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ <p>
+ <i18n.Translate>Refunds</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.refund}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>{" "}
+ <p>
+ <i18n.Translate>Refresh</i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Denomination</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue
+ list={selected.denomFees.refresh}
+ sorting={(a, b) => Number(a) - Number(b)}
+ />
+ </tbody>
+ </FeeDescriptionTable>
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Transfer operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by transfer type
+ and is valid for a period of time. The exchange will charge the
+ indicated amount every time a transfer is made.
+ </i18n.Translate>
+ </p>
+ {Object.entries(selected.transferFees).map(([type, fees], idx) => {
+ return (
+ <Fragment key={idx}>
+ <p>{type}</p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Operation</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue list={fees} />
+ </tbody>
+ </FeeDescriptionTable>
+ </Fragment>
+ );
+ })}
+ </section>
+ <section>
+ <h2>
+ <i18n.Translate>Wallet operations</i18n.Translate>
+ </h2>
+ <p>
+ <i18n.Translate>
+ Every operation in this section may be different by transfer type
+ and is valid for a period of time. The exchange will charge the
+ indicated amount every time a transfer is made.
+ </i18n.Translate>
+ </p>
+ <FeeDescriptionTable>
+ <thead>
+ <tr>
+ <th>&nbsp;</th>
+ <th>
+ <i18n.Translate>Feature</i18n.Translate>
+ </th>
+ <th class="fee">
+ <i18n.Translate>Fee</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Until</i18n.Translate>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <RenderFeeDescriptionByValue list={selected.globalFees} />
+ </tbody>
+ </FeeDescriptionTable>
+ </section>
+ <section>
+ <ButtonGroupFooter>
+ <Button onClick={onShowPrivacy.onClick} variant="outlined">
+ Privacy policy
+ </Button>
+ <Button onClick={onShowTerms.onClick} variant="outlined">
+ Terms of service
+ </Button>
+ </ButtonGroupFooter>
+ </section>
+ </Container>
+ );
+}
+
+function FeeDescriptionRowsGroup({
+ infos,
+}: {
+ infos: FeeDescription[];
+}): VNode {
+ const [expanded, setExpand] = useState(false);
+ const hasMoreInfo = infos.length > 1;
+ return (
+ <Fragment>
+ {infos.map((info, idx) => {
+ const main = idx === 0;
+ return (
+ <tr
+ key={idx}
+ class="value"
+ data-hasMore={hasMoreInfo}
+ data-main={main}
+ data-hidden={!main && !expanded}
+ onClick={() => setExpand((p) => !p)}
+ >
+ <td class="icon">
+ {hasMoreInfo && main ? (
+ <SvgIcon
+ title="Select this contact"
+ dangerouslySetInnerHTML={{ __html: arrowDown }}
+ color="currentColor"
+ transform={expanded ? "" : "rotate(-90deg)"}
+ />
+ ) : undefined}
+ </td>
+ <td class="value">{main ? info.group : ""}</td>
+ {info.fee ? (
+ <td class="fee">{<Amount value={info.fee} hideCurrency />}</td>
+ ) : undefined}
+ <td class="expiration">
+ <Time timestamp={info.until} format="dd-MMM-yyyy" />
+ </td>
+ </tr>
+ );
+ })}
+ </Fragment>
+ );
+}
+
+function FeePairRowsGroup({ infos }: { infos: FeeDescriptionPair[] }): VNode {
+ const [expanded, setExpand] = useState(false);
+ const hasMoreInfo = infos.length > 1;
+ return (
+ <Fragment>
+ {infos.map((info, idx) => {
+ const main = idx === 0;
+ return (
+ <tr
+ key={idx}
+ class="value"
+ data-hasMore={hasMoreInfo}
+ data-main={main}
+ data-hidden={!main && !expanded}
+ onClick={() => setExpand((p) => !p)}
+ >
+ <td class="icon">
+ {hasMoreInfo && main ? (
+ <SvgIcon
+ title="Expand"
+ dangerouslySetInnerHTML={{ __html: arrowDown }}
+ color="currentColor"
+ transform={expanded ? "" : "rotate(-90deg)"}
+ />
+ ) : undefined}
+ </td>
+ <td class="value">{main ? info.group : ""}</td>
+ {info.left ? (
+ <td class="fee">{<Amount value={info.left} hideCurrency />}</td>
+ ) : (
+ <td class="fee"> --- </td>
+ )}
+ {info.right ? (
+ <td class="fee">{<Amount value={info.right} hideCurrency />}</td>
+ ) : (
+ <td class="fee"> --- </td>
+ )}
+ <td class="expiration">
+ <Time timestamp={info.until} format="dd-MMM-yyyy HH:mm:ss" />
+ </td>
+ </tr>
+ );
+ })}
+ </Fragment>
+ );
+}
+
+/**
+ * Group by value and then render using FeePairRowsGroup
+ * @param param0
+ * @returns
+ */
+function RenderFeePairByValue({
+ list,
+ sorting,
+}: {
+ list: FeeDescriptionPair[];
+ sorting?: (a: string, b: string) => number;
+}): VNode {
+ const grouped = list.reduce((prev, cur) => {
+ if (!prev[cur.group]) {
+ prev[cur.group] = [];
+ }
+ prev[cur.group].push(cur);
+ return prev;
+ }, {} as Record<string, FeeDescriptionPair[]>);
+ const p = Object.keys(grouped)
+ .sort(sorting)
+ .map((i, idx) => <FeePairRowsGroup key={idx} infos={grouped[i]} />);
+ return <Fragment>{p}</Fragment>;
+}
+/**
+ *
+ * Group by value and then render using FeeDescriptionRowsGroup
+ * @param param0
+ * @returns
+ */
+function RenderFeeDescriptionByValue({
+ list,
+ sorting,
+}: {
+ list: FeeDescription[];
+ sorting?: (a: string, b: string) => number;
+}): VNode {
+ const grouped = list.reduce((prev, cur) => {
+ if (!prev[cur.group]) {
+ prev[cur.group] = [];
+ }
+ prev[cur.group].push(cur);
+ return prev;
+ }, {} as Record<string, FeeDescription[]>);
+ const p = Object.keys(grouped)
+ .sort(sorting)
+ .map((i, idx) => <FeeDescriptionRowsGroup key={idx} infos={grouped[i]} />);
+ return <Fragment>{p}</Fragment>;
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
index 0ac4be9a6..482b8d698 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,184 +15,608 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import {
+ AmountString,
PaymentStatus,
- TransactionCommon, TransactionDeposit, TransactionPayment,
- TransactionRefresh, TransactionRefund, TransactionTip, TransactionType,
+ RefreshReason,
+ ScopeType,
+ TalerProtocolTimestamp,
+ TransactionCommon,
+ TransactionDeposit,
+ TransactionMajorState,
+ TransactionPayment,
+ TransactionPeerPullCredit,
+ TransactionPeerPullDebit,
+ TransactionPeerPushCredit,
+ TransactionPeerPushDebit,
+ TransactionRefresh,
+ TransactionRefund,
+ TransactionType,
TransactionWithdrawal,
- WithdrawalType
-} from '@gnu-taler/taler-util';
-import { HistoryView as TestedComponent } from './History';
-import { createExample } from '../test-utils';
-
+ WithdrawalType,
+} from "@gnu-taler/taler-util";
+import { HistoryView as TestedComponent } from "./History.js";
+import * as tests from "@gnu-taler/web-util/testing";
export default {
- title: 'wallet/history/list',
+ title: "history",
component: TestedComponent,
};
-let count = 0
-const commonTransaction = () => ({
- amountRaw: 'USD:10',
- amountEffective: 'USD:9',
- pending: false,
- timestamp: {
- t_ms: new Date().getTime() - (count++ * 1000 * 60 * 60 * 7)
- },
- transactionId: '12',
-} as TransactionCommon)
+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;
const exampleData = {
withdraw: {
...commonTransaction(),
type: TransactionType.Withdrawal,
- exchangeBaseUrl: 'http://exchange.demo.taler.net',
+ exchangeBaseUrl: "http://exchange.demo.taler.net",
withdrawalDetails: {
+ reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
confirmed: false,
- exchangePaytoUris: ['payto://x-taler-bank/bank/account'],
+ exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
type: WithdrawalType.ManualTransfer,
- }
+ reserveIsReady: false,
+ },
} as TransactionWithdrawal,
payment: {
...commonTransaction(),
- amountEffective: 'USD:11',
+ amountEffective: "USD:11" as AmountString,
type: TransactionType.Payment,
+ posConfirmation: undefined,
info: {
- contractTermsHash: 'ASDZXCASD',
+ contractTermsHash: "ASDZXCASD",
merchant: {
- name: 'Blog',
+ name: "Blog",
},
- orderId: '2021.167-03NPY6MCYMVGT',
+ orderId: "2021.167-03NPY6MCYMVGT",
products: [],
- summary: 'the summary',
- fulfillmentMessage: '',
+ summary: "the summary",
+ fulfillmentMessage: "",
},
- proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
+ refunds: [],
+ refundPending: undefined,
+ totalRefundEffective: "USD:0" as AmountString,
+ totalRefundRaw: "USD:0" as AmountString,
+ proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
+ refundQueryActive: false,
} as TransactionPayment,
deposit: {
...commonTransaction(),
type: TransactionType.Deposit,
- depositGroupId: '#groupId',
- targetPaytoUri: 'payto://x-taler-bank/bank/account',
+ depositGroupId: "#groupId",
+ targetPaytoUri: "payto://x-taler-bank/bank/account",
} as TransactionDeposit,
refresh: {
...commonTransaction(),
type: TransactionType.Refresh,
- exchangeBaseUrl: 'http://exchange.taler',
+ refreshInputAmount: "USD:1" as AmountString,
+ refreshOutputAmount: "USD:0.5" as AmountString,
+ exchangeBaseUrl: "http://exchange.taler",
+ refreshReason: RefreshReason.PayMerchant,
} as TransactionRefresh,
- tip: {
- ...commonTransaction(),
- type: TransactionType.Tip,
- merchantBaseUrl: 'http://ads.merchant.taler.net/',
- } as TransactionTip,
refund: {
...commonTransaction(),
type: TransactionType.Refund,
- refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
- info: {
- contractTermsHash: 'ASDZXCASD',
+ refundedTransactionId:
+ "payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
+ paymentInfo: {
merchant: {
- name: 'the merchant',
+ name: "the merchant",
},
- orderId: '2021.167-03NPY6MCYMVGT',
- products: [],
- summary: 'the summary',
- fulfillmentMessage: '',
+ summary: "the summary",
},
+ refundPending: undefined,
} as TransactionRefund,
-}
-
-export const Empty = createExample(TestedComponent, {
- list: [],
- balances: [{
- available: 'TESTKUDOS:10',
- pendingIncoming: 'TESTKUDOS:0',
- pendingOutgoing: 'TESTKUDOS:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
-});
+ push_credit: {
+ ...commonTransaction(),
+ type: TransactionType.PeerPushCredit,
+ info: {
+ summary: "take this cash",
+ },
+ exchangeBaseUrl: "https://exchange.taler.net",
+ } as TransactionPeerPushCredit,
+ push_debit: {
+ ...commonTransaction(),
+ type: TransactionType.PeerPushDebit,
+ talerUri:
+ "taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
+ info: {
+ summary: "take this cash",
+ },
+ exchangeBaseUrl: "https://exchange.taler.net",
+ } as TransactionPeerPushDebit,
+ pull_credit: {
+ ...commonTransaction(),
+ type: TransactionType.PeerPullCredit,
+ talerUri:
+ "taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
+ info: {
+ summary: "pay me",
+ },
+ exchangeBaseUrl: "https://exchange.taler.net",
+ } as TransactionPeerPullCredit,
+ pull_debit: {
+ ...commonTransaction(),
+ type: TransactionType.PeerPullDebit,
+ info: {
+ summary: "pay me",
+ },
+ exchangeBaseUrl: "https://exchange.taler.net",
+ } as TransactionPeerPullDebit,
+};
+export const SomeBalanceWithNoTransactions = tests.createExample(
+ TestedComponent,
+ {
+ transactionsByDate: {
+ "11/11/11": [],
+ },
+ balances: [
+ {
+ available: "TESTKUDOS:10" as AmountString,
+ flags: [],
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ balanceIndex: 0,
+ },
+);
-export const One = createExample(TestedComponent, {
- list: [exampleData.withdraw],
- balances: [{
- available: 'USD:10',
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
+export const OneSimpleTransaction = tests.createExample(TestedComponent, {
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
+ balances: [
+ {
+ flags: [],
+ available: "USD:10" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ balanceIndex: 0,
});
-export const OnePending = createExample(TestedComponent, {
- list: [{
- ...exampleData.withdraw,
- pending: true
- }],
- balances: [{
- available: 'USD:10',
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
+export const TwoTransactionsAndZeroBalance = tests.createExample(
+ TestedComponent,
+ {
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw, exampleData.deposit],
+ },
+ balances: [
+ {
+ flags: [],
+ available: "USD:0" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ balanceIndex: 0,
+ },
+);
+
+export const OneTransactionPending = tests.createExample(TestedComponent, {
+ transactionsByDate: {
+ "11/11/11": [
+ {
+ ...exampleData.withdraw,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+ ],
+ },
+ balances: [
+ {
+ flags: [],
+ available: "USD:10" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ balanceIndex: 0,
});
-export const Several = createExample(TestedComponent, {
- list: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
+export const SomeTransactions = tests.createExample(TestedComponent, {
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.withdraw,
+ exampleData.payment,
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary:
+ "this is a long summary that may be cropped because its too long",
+ },
+ },
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
+ balances: [
{
- ...exampleData.payment,
- info: {
- ...exampleData.payment.info,
- summary: 'this is a long summary that may be cropped because its too long',
+ flags: [],
+ available: "USD:10" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
},
},
- exampleData.refund,
- exampleData.tip,
- exampleData.deposit,
],
- balances: [{
- available: 'TESTKUDOS:10',
- pendingIncoming: 'TESTKUDOS:0',
- pendingOutgoing: 'TESTKUDOS:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
+ balanceIndex: 0,
});
-export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
- list: [
- exampleData.withdraw,
- exampleData.payment,
- exampleData.withdraw,
- exampleData.payment,
- exampleData.refresh,
- exampleData.refund,
- exampleData.tip,
- exampleData.deposit,
+export const SomeTransactionsInDifferentStates = tests.createExample(
+ TestedComponent,
+ {
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://aborted/withdrawal",
+ txState: {
+ major: TransactionMajorState.Aborted,
+ },
+ },
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://pending/withdrawal",
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+ {
+ ...exampleData.withdraw,
+ exchangeBaseUrl: "https://failed/withdrawal",
+ txState: {
+ major: TransactionMajorState.Failed,
+ },
+ },
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "normal payment",
+ },
+ },
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "aborting in progress",
+ },
+ txState: {
+ major: TransactionMajorState.Aborting,
+ },
+ },
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "aborted payment",
+ },
+ txState: {
+ major: TransactionMajorState.Aborted,
+ },
+ },
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "pending payment",
+ },
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+ {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "failed payment",
+ },
+ txState: {
+ major: TransactionMajorState.Failed,
+ },
+ },
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
+ balances: [
+ {
+ flags: [],
+ available: "USD:10" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ balanceIndex: 0,
+ },
+);
+
+export const SomeTransactionsWithTwoCurrencies = tests.createExample(
+ TestedComponent,
+ {
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.withdraw,
+ exampleData.payment,
+ exampleData.refresh,
+ exampleData.refund,
+ exampleData.deposit,
+ ],
+ },
+ balances: [
+ {
+ flags: [],
+ available: "USD:0" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ {
+ flags: [],
+ available: "TESTKUDOS:10" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ balanceIndex: 0,
+ },
+);
+
+export const FiveOfficialCurrencies = tests.createExample(TestedComponent, {
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
+ balances: [
+ {
+ flags: [],
+ available: "USD:1000" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ {
+ flags: [],
+ available: "EUR:881" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ {
+ flags: [],
+ available: "COL:4043000.5" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ {
+ flags: [],
+ available: "JPY:11564450.6" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ {
+ flags: [],
+ available: "GBP:736" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
],
- balances: [{
- available: 'TESTKUDOS:10',
- pendingIncoming: 'TESTKUDOS:0',
- pendingOutgoing: 'TESTKUDOS:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }, {
- available: 'USD:10',
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
+ balanceIndex: 0,
});
+export const FiveOfficialCurrenciesWithHighValue = tests.createExample(
+ TestedComponent,
+ {
+ transactionsByDate: {
+ "11/11/11": [exampleData.withdraw],
+ },
+ balances: [
+ {
+ flags: [],
+ available: "USD:881001321230000" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ {
+ flags: [],
+ available: "EUR:10" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ {
+ flags: [],
+ available: "COL:443000123123000.5123123" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ requiresUserInput: false,
+ },
+ {
+ flags: [],
+ available: "JPY:1564450000000.6123123" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ {
+ flags: [],
+ available: "GBP:736001231231200.23123" as AmountString,
+ pendingIncoming: "TESTKUDOS:0" as AmountString,
+ pendingOutgoing: "TESTKUDOS:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ balanceIndex: 0,
+ },
+);
+
+export const PeerToPeer = tests.createExample(TestedComponent, {
+ transactionsByDate: {
+ "11/11/11": [
+ exampleData.pull_credit,
+ exampleData.pull_debit,
+ exampleData.push_credit,
+ exampleData.push_debit,
+ ],
+ },
+ balances: [
+ {
+ flags: [],
+ available: "USD:10" as AmountString,
+ pendingIncoming: "USD:0" as AmountString,
+ pendingOutgoing: "USD:0" as AmountString,
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ scopeInfo: {
+ currency: "Ásd",
+ type: ScopeType.Auditor,
+ url: "",
+ },
+ },
+ ],
+ balanceIndex: 0,
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx
index 8160f8574..f81e6db9f 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.tsx
@@ -1,87 +1,371 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountString, Balance, Transaction, TransactionsResponse } from "@gnu-taler/taler-util";
-import { format } from "date-fns";
-import { Fragment, h, JSX } from "preact";
+import {
+ AbsoluteTime,
+ Amounts,
+ NotificationType,
+ ScopeType,
+ Transaction,
+ WalletBalance,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { startOfDay } from "date-fns";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
-import { DateSeparator, WalletBox } from "../components/styled";
-import { TransactionItem } from "../components/TransactionItem";
-import { useBalances } from "../hooks/useBalances";
-import * as wxApi from "../wxApi";
+import { ErrorAlertView } from "../components/CurrentAlerts.js";
+import { HistoryItem } from "../components/HistoryItem.js";
+import { Loading } from "../components/Loading.js";
+import { Time } from "../components/Time.js";
+import {
+ CenteredBoldText,
+ CenteredText,
+ DateSeparator,
+ NiceSelect,
+} from "../components/styled/index.js";
+import { alertFromError, useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
+import { Button } from "../mui/Button.js";
+import { NoBalanceHelp } from "../popup/NoBalanceHelp.js";
+import DownloadIcon from "../svg/download_24px.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: _c,
+ search: showSearch,
+ goToWalletManualWithdraw,
+ goToWalletDeposit,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+ const [balanceIndex, setBalanceIndex] = useState<number>(0);
+ const [search, setSearch] = useState<string>();
-export function HistoryPage(props: any): JSX.Element {
- const [transactions, setTransactions] = useState<
- TransactionsResponse | undefined
- >(undefined);
- const balance = useBalances()
- const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || [])
+ const [settings] = useSettings();
+ const state = useAsyncAsHook(async () => {
+ const b = await api.wallet.call(WalletApiOperation.GetBalances, {});
+ const balance =
+ b.balances.length > 0 ? b.balances[balanceIndex] : undefined;
+ const tx = await api.wallet.call(WalletApiOperation.GetTransactions, {
+ scopeInfo: showSearch ? undefined : balance?.scopeInfo,
+ sort: "descending",
+ includeRefreshes: settings.showRefeshTransactions,
+ search,
+ });
+ return { b, tx };
+ }, [balanceIndex, search]);
useEffect(() => {
- const fetchData = async (): Promise<void> => {
- const res = await wxApi.getTransactions();
- setTransactions(res);
- };
- fetchData();
- }, []);
-
- if (!transactions) {
- return <div>Loading ...</div>;
+ return api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
+ state?.retry,
+ );
+ });
+ const { pushAlertOnError } = useAlertContext();
+
+ if (!state) {
+ return <Loading />;
+ }
+
+ if (state.hasError) {
+ return (
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not load the list of transactions`,
+ state,
+ )}
+ />
+ );
+ }
+
+ if (!state.response.b.balances.length) {
+ return (
+ <NoBalanceHelp
+ goToWalletManualWithdraw={{
+ onClick: pushAlertOnError(goToWalletManualWithdraw),
+ }}
+ />
+ );
+ }
+
+ const byDate = state.response.tx.transactions.reduce(
+ (rv, x) => {
+ const startDay =
+ x.timestamp.t_s === "never"
+ ? 0
+ : startOfDay(x.timestamp.t_s * 1000).getTime();
+ if (startDay) {
+ if (!rv[startDay]) {
+ rv[startDay] = [];
+ // datesWithTransaction.push(String(startDay));
+ }
+ 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 balances={balanceWithoutError} list={[...transactions.transactions].reverse()} />;
+ return (
+ <HistoryView
+ balanceIndex={balanceIndex}
+ changeBalanceIndex={(b) => setBalanceIndex(b)}
+ balances={state.response.b.balances}
+ goToWalletManualWithdraw={goToWalletManualWithdraw}
+ goToWalletDeposit={goToWalletDeposit}
+ transactionsByDate={byDate}
+ />
+ );
}
-function amountToString(c: AmountString) {
- const idx = c.indexOf(':')
- return `${c.substring(idx + 1)} ${c.substring(0, idx)}`
+export function HistoryView({
+ balances,
+ balanceIndex,
+ changeBalanceIndex,
+ transactionsByDate,
+ goToWalletManualWithdraw,
+ goToWalletDeposit,
+}: {
+ balanceIndex: number;
+ changeBalanceIndex: (s: number) => void;
+ goToWalletDeposit: (currency: string) => Promise<void>;
+ goToWalletManualWithdraw: (currency?: string) => Promise<void>;
+ transactionsByDate: Record<string, Transaction[]>;
+ balances: WalletBalance[];
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const balance = balances[balanceIndex];
+
+ const available = balance
+ ? Amounts.jsonifyAmount(balance.available)
+ : undefined;
+
+ const datesWithTransaction: string[] = Object.keys(transactionsByDate);
+
+ return (
+ <Fragment>
+ <section>
+ <div
+ style={{
+ display: "flex",
+ flexWrap: "wrap",
+ alignItems: "center",
+ justifyContent: "space-between",
+ marginRight: 20,
+ }}
+ >
+ <div>
+ <Button
+ tooltip="Transfer money to the wallet"
+ startIcon={DownloadIcon}
+ variant="contained"
+ onClick={() =>
+ goToWalletManualWithdraw(balance.scopeInfo.currency)
+ }
+ >
+ <i18n.Translate>Receive</i18n.Translate>
+ </Button>
+ {available && Amounts.isNonZero(available) && (
+ <Button
+ tooltip="Transfer money from the wallet"
+ startIcon={UploadIcon}
+ variant="outlined"
+ color="primary"
+ onClick={() => goToWalletDeposit(balance.scopeInfo.currency)}
+ >
+ <i18n.Translate>Send</i18n.Translate>
+ </Button>
+ )}
+ </div>
+ <div style={{ display: "flex", flexDirection: "column" }}>
+ <h3 style={{ marginBottom: 0 }}>Balance</h3>
+ <div
+ style={{
+ width: "fit-content",
+ display: "flex",
+ }}
+ >
+ {balances.length === 1 ? (
+ <CenteredText style={{ fontSize: "x-large", margin: 8 }}>
+ {balance.scopeInfo.currency}
+ </CenteredText>
+ ) : (
+ <NiceSelect style={{ flexDirection: "column" }}>
+ <select
+ style={{
+ fontSize: "x-large",
+ }}
+ value={balanceIndex}
+ onChange={(e) => {
+ changeBalanceIndex(
+ Number.parseInt(e.currentTarget.value, 10),
+ );
+ }}
+ >
+ {balances.map((entry, index) => {
+ return (
+ <option value={index} key={entry.scopeInfo.currency}>
+ {entry.scopeInfo.currency}
+ </option>
+ );
+ })}
+ </select>
+ <div style={{ fontSize: "small", color: "grey" }}>
+ {balance.scopeInfo.type === ScopeType.Exchange ||
+ balance.scopeInfo.type === ScopeType.Auditor
+ ? balance.scopeInfo.url
+ : undefined}
+ </div>
+ </NiceSelect>
+ )}
+ {available && (
+ <CenteredBoldText
+ style={{
+ display: "inline-block",
+ fontSize: "x-large",
+ margin: 8,
+ }}
+ >
+ {Amounts.stringifyValue(available, 2)}
+ </CenteredBoldText>
+ )}
+ </div>
+ </div>
+ </div>
+ </section>
+ {datesWithTransaction.length === 0 ? (
+ <section>
+ <i18n.Translate>
+ Your transaction history is empty for this currency.
+ </i18n.Translate>
+ </section>
+ ) : (
+ <section>
+ {datesWithTransaction.map((d, i) => {
+ return (
+ <Fragment key={i}>
+ <DateSeparator>
+ <Time
+ timestamp={AbsoluteTime.fromMilliseconds(
+ Number.parseInt(d, 10),
+ )}
+ format="dd MMMM yyyy"
+ />
+ </DateSeparator>
+ {transactionsByDate[d].map((tx, i) => (
+ <HistoryItem key={i} tx={tx} />
+ ))}
+ </Fragment>
+ );
+ })}
+ </section>
+ )}
+ </Fragment>
+ );
}
+export function FilteredHistoryView({
+ search,
+ transactionsByDate,
+}: {
+ search: TextFieldHandler;
+ transactionsByDate: Record<string, Transaction[]>;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const datesWithTransaction: string[] = Object.keys(transactionsByDate);
-export function HistoryView({ list, balances }: { list: Transaction[], balances: Balance[] }) {
- const byDate = list.reduce(function (rv, x) {
- const theDate = x.timestamp.t_ms === "never" ? "never" : format(x.timestamp.t_ms, 'dd MMMM yyyy');
- (rv[theDate] = rv[theDate] || []).push(x);
- return rv;
- }, {} as { [x: string]: Transaction[] });
-
- const multiCurrency = balances.length > 1
-
- return <WalletBox noPadding>
- {balances.length > 0 && <header>
- {balances.length === 1 && <div class="title">
- Balance: <span>{amountToString(balances[0].available)}</span>
- </div>}
- {balances.length > 1 && <div class="title">
- Balance: <ul style={{ margin: 0 }}>
- {balances.map(b => <li>{b.available}</li>)}
- </ul>
- </div>}
- </header>}
- <section>
- {Object.keys(byDate).map((d,i) => {
- return <Fragment key={i}>
- <DateSeparator>{d}</DateSeparator>
- {byDate[d].map((tx, i) => (
- <TransactionItem key={i} tx={tx} multiCurrency={multiCurrency}/>
- ))}
- </Fragment>
- })}
- </section>
- </WalletBox>
+ 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 ? (
+ <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>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts
new file mode 100644
index 000000000..3a00d48ce
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts
@@ -0,0 +1,80 @@
+/*
+ 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 { KnownBankAccountsInfo } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import {
+ ButtonHandler,
+ SelectFieldHandler,
+ TextFieldHandler,
+} from "../../mui/handlers.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+
+export interface Props {
+ currency: string;
+ onAccountAdded: (uri: string) => void;
+ onCancel: () => void;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ currency: string;
+ accountType: SelectFieldHandler;
+ uri: TextFieldHandler;
+ alias: TextFieldHandler;
+ onAccountAdded: ButtonHandler;
+ onCancel: ButtonHandler;
+ accountByType: AccountByType;
+ deleteAccount: (a: KnownBankAccountsInfo) => Promise<void>;
+ }
+}
+
+export type AccountByType = {
+ [key: string]: KnownBankAccountsInfo[];
+};
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ ready: ReadyView,
+};
+
+export const ManageAccountPage = compose(
+ "ManageAccountPage",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
new file mode 100644
index 000000000..a7b2fe90f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts
@@ -0,0 +1,145 @@
+/*
+ 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 {
+ KnownBankAccountsInfo,
+ parsePaytoUri,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useState } from "preact/hooks";
+import { alertFromError, useAlertContext } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { AccountByType, Props, State } from "./index.js";
+import { useSettings } from "../../hooks/useSettings.js";
+
+export function useComponentState({
+ currency,
+ onAccountAdded,
+ onCancel,
+}: Props): State {
+ const api = useBackendContext();
+ const { pushAlertOnError } = useAlertContext();
+ const { i18n } = useTranslationContext();
+ const hook = useAsyncAsHook(() =>
+ api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }),
+ );
+ const accountType: Record<string, string> = {
+ iban: "IBAN",
+ };
+ const [settings] = useSettings();
+ if (settings.extendedAccountTypes) {
+ accountType["bitcoin"] = "Bitcoin";
+ accountType["x-taler-bank"] = "Taler Bank";
+ }
+
+ const [payto, setPayto] = useState("");
+ const [alias, setAlias] = useState("");
+ const [type, setType] = useState("iban");
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load known bank accounts`,
+ hook),
+ };
+ }
+
+ const uri = parsePaytoUri(payto);
+ const found =
+ hook.response.accounts.findIndex(
+ (a) => stringifyPaytoUri(a.uri) === payto,
+ ) !== -1;
+
+ async function addAccount(): Promise<void> {
+ if (!uri || found) return;
+
+ const normalizedPayto = stringifyPaytoUri(uri);
+ await api.wallet.call(WalletApiOperation.AddKnownBankAccounts, {
+ alias,
+ currency,
+ payto: normalizedPayto,
+ });
+ onAccountAdded(payto);
+ }
+
+ const paytoUriError = found ? "that account is already present" : undefined;
+
+ const unableToAdd =
+ !type || !alias || paytoUriError !== undefined || uri === undefined;
+
+ const accountByType: AccountByType = {
+ iban: [],
+ bitcoin: [],
+ "x-taler-bank": [],
+ };
+
+ hook.response.accounts.forEach((acc) => {
+ accountByType[acc.uri.targetType].push(acc);
+ });
+
+ async function deleteAccount(account: KnownBankAccountsInfo): Promise<void> {
+ const payto = stringifyPaytoUri(account.uri);
+ await api.wallet.call(WalletApiOperation.ForgetKnownBankAccounts, {
+ payto,
+ });
+ hook?.retry();
+ }
+
+ return {
+ status: "ready",
+ error: undefined,
+ currency,
+ accountType: {
+ list: accountType,
+ value: type,
+ onChange: pushAlertOnError(async (v) => {
+ setType(v);
+ }),
+ },
+ alias: {
+ value: alias,
+ onInput: pushAlertOnError(async (v) => {
+ setAlias(v);
+ }),
+ },
+ uri: {
+ value: payto,
+ error: paytoUriError,
+ onInput: pushAlertOnError(async (v) => {
+ setPayto(v);
+ }),
+ },
+ accountByType,
+ deleteAccount: pushAlertOnError(deleteAccount),
+ onAccountAdded: {
+ onClick: unableToAdd ? undefined : pushAlertOnError(addAccount),
+ },
+ onCancel: {
+ onClick: pushAlertOnError(async () => onCancel()),
+ },
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
new file mode 100644
index 000000000..c01797e31
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx
@@ -0,0 +1,207 @@
+/*
+ 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 { nullFunction } from "../../mui/handlers.js";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "manage account",
+};
+
+export const JustTwoBitcoinAccounts = tests.createExample(ReadyView, {
+ status: "ready",
+ currency: "ARS",
+ accountType: {
+ list: {
+ iban: "IBAN",
+ bitcoin: "Bitcoin",
+ "x-taler-bank": "Taler Bank",
+ },
+ value: "bitcoin",
+ },
+ alias: {
+ value: "",
+ onInput: nullFunction,
+ },
+ uri: {
+ value: "",
+ onInput: nullFunction,
+ },
+ accountByType: {
+ iban: [],
+ "x-taler-bank": [],
+ bitcoin: [
+ {
+ alias: "my bitcoin addr",
+ currency: "BTC",
+ kyc_completed: false,
+ uri: {
+ targetType: "bitcoin",
+ segwitAddrs: [],
+ isKnown: true,
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
+ targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
+ params: {},
+ },
+ },
+ {
+ alias: "my other addr",
+ currency: "BTC",
+ kyc_completed: true,
+ uri: {
+ targetType: "bitcoin",
+ segwitAddrs: [],
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
+ isKnown: true,
+ targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
+ params: {},
+ },
+ },
+ ],
+ },
+ onAccountAdded: {},
+ onCancel: {},
+});
+
+export const WithAllTypeOfAccounts = tests.createExample(ReadyView, {
+ status: "ready",
+ currency: "ARS",
+ accountType: {
+ list: {
+ iban: "IBAN",
+ bitcoin: "Bitcoin",
+ "x-taler-bank": "Taler Bank",
+ },
+ value: "x-taler-bank",
+ },
+ alias: {
+ value: "",
+ onInput: nullFunction,
+ },
+ uri: {
+ value: "",
+ onInput: nullFunction,
+ },
+ accountByType: {
+ iban: [
+ {
+ alias: "my bank",
+ currency: "ARS",
+ kyc_completed: true,
+ uri: {
+ targetType: "iban",
+ iban: "ASDQWEQWE",
+ isKnown: true,
+ targetPath: "/ASDQWEQWE",
+ params: {},
+ },
+ },
+ ],
+ "x-taler-bank": [
+ {
+ alias: "my xtaler bank",
+ currency: "ARS",
+ kyc_completed: true,
+ uri: {
+ targetType: "x-taler-bank",
+ host: "localhost",
+ account: "123",
+ isKnown: true,
+ targetPath: "localhost/123",
+ params: {},
+ },
+ },
+ ],
+ bitcoin: [
+ {
+ alias: "my bitcoin addr",
+ currency: "BTC",
+ kyc_completed: false,
+ uri: {
+ targetType: "bitcoin",
+ segwitAddrs: [],
+ isKnown: true,
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
+ targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
+ params: {},
+ },
+ },
+ {
+ alias: "my other addr",
+ currency: "BTC",
+ kyc_completed: true,
+ uri: {
+ targetType: "bitcoin",
+ segwitAddrs: [],
+ isKnown: true,
+ address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
+ targetPath: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
+ params: {},
+ },
+ },
+ ],
+ },
+ onAccountAdded: {},
+ onCancel: {},
+});
+
+export const AddingIbanAccount = tests.createExample(ReadyView, {
+ status: "ready",
+ currency: "ARS",
+ accountType: {
+ list: {
+ iban: "IBAN",
+ // bitcoin: "Bitcoin",
+ // "x-taler-bank": "Taler Bank",
+ },
+ value: "iban",
+ },
+ alias: {
+ value: "",
+ onInput: nullFunction,
+ },
+ uri: {
+ value: "",
+ onInput: nullFunction,
+ },
+ accountByType: {
+ iban: [
+ {
+ alias: "my bank",
+ currency: "ARS",
+ kyc_completed: true,
+ uri: {
+ targetType: "iban",
+ iban: "ASDQWEQWE",
+ bic: "SANDBOX",
+ isKnown: true,
+ targetPath: "SANDBOX/ASDQWEQWE",
+ params: {},
+ },
+ },
+ ],
+ "x-taler-bank": [],
+ bitcoin: [],
+ },
+ onAccountAdded: {},
+ onCancel: {},
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/test.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/test.ts
new file mode 100644
index 000000000..868269ec0
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/test.ts
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("Manage Account states", () => {
+ it.skip("should create some tests", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
new file mode 100644
index 000000000..7b80977f3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx
@@ -0,0 +1,602 @@
+/*
+ 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 {
+ buildPayto,
+ KnownBankAccountsInfo,
+ PaytoUriBitcoin,
+ PaytoUriIBAN,
+ PaytoUriTalerBank,
+ stringifyPaytoUri,
+ validateIban,
+} from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { styled } from "@linaria/react";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorMessage } from "../../components/ErrorMessage.js";
+import { SubTitle, SvgIcon } from "../../components/styled/index.js";
+import { Button } from "../../mui/Button.js";
+import { TextFieldHandler } from "../../mui/handlers.js";
+import { TextField } from "../../mui/TextField.js";
+import checkIcon from "../../svg/check_24px.inline.svg";
+import deleteIcon from "../../svg/delete_24px.inline.svg";
+import warningIcon from "../../svg/warning_24px.inline.svg";
+import { State } from "./index.js";
+
+type AccountType = "bitcoin" | "x-taler-bank" | "iban";
+type ComponentFormByAccountType = {
+ [type in AccountType]: (props: { field: TextFieldHandler }) => VNode;
+};
+
+type ComponentListByAccountType = {
+ [type in AccountType]: (props: {
+ list: KnownBankAccountsInfo[];
+ onDelete: (a: KnownBankAccountsInfo) => Promise<void>;
+ }) => VNode;
+};
+
+const formComponentByAccountType: ComponentFormByAccountType = {
+ iban: IbanAddressAccount,
+ bitcoin: BitcoinAddressAccount,
+ "x-taler-bank": TalerBankAddressAccount,
+};
+const tableComponentByAccountType: ComponentListByAccountType = {
+ iban: IbanTable,
+ bitcoin: BitcoinTable,
+ "x-taler-bank": TalerBankTable,
+};
+
+const AccountTable = styled.table`
+ width: 100%;
+
+ border-collapse: separate;
+ border-spacing: 0px 10px;
+ tbody tr:nth-child(odd) > td:not(.actions, .kyc) {
+ background-color: lightgrey;
+ }
+ .actions,
+ .kyc {
+ width: 10px;
+ background-color: inherit;
+ }
+`;
+
+export function ReadyView({
+ currency,
+ error,
+ accountType,
+ accountByType,
+ alias,
+ onAccountAdded,
+ deleteAccount,
+ onCancel,
+ uri,
+}: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <Fragment>
+ <section>
+ <SubTitle>
+ <i18n.Translate>Known accounts for {currency}</i18n.Translate>
+ </SubTitle>
+ <p>
+ <i18n.Translate>
+ To add a new account first select the account type.
+ </i18n.Translate>
+ </p>
+
+ {error && (
+ <ErrorMessage
+ title={i18n.str`Unable add this account`}
+ description={error}
+ />
+ )}
+ <div style={{ width: "100%", display: "flex" }}>
+ {Object.entries(accountType.list).map(([key, name], idx) => (
+ <div
+ key={idx}
+ style={{
+ marginLeft: 8,
+ padding: 8,
+ borderTopLeftRadius: 5,
+ borderTopRightRadius: 5,
+ backgroundColor:
+ accountType.value === key ? "#0042b2" : "unset",
+ color: accountType.value === key ? "white" : "unset",
+ }}
+ onClick={() => {
+ if (accountType.onChange) {
+ accountType.onChange(key);
+ }
+ }}
+ >
+ {name}
+ </div>
+ ))}
+ </div>
+ <div style={{ border: "1px solid gray", padding: 8, borderRadius: 5 }}>
+ --- {uri.value} ---
+ <p>
+ <CustomFieldByAccountType
+ type={accountType.value as AccountType}
+ field={uri}
+ />
+ </p>
+ </div>
+ <p>
+ <TextField
+ label="Alias"
+ variant="filled"
+ placeholder="Easy to remember description"
+ fullWidth
+ disabled={accountType.value === ""}
+ value={alias.value}
+ onChange={alias.onInput}
+ />
+ </p>
+ </section>
+ <section>
+ <Button
+ variant="contained"
+ color="secondary"
+ onClick={onCancel.onClick}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ <Button
+ variant="contained"
+ onClick={onAccountAdded.onClick}
+ disabled={!onAccountAdded.onClick}
+ >
+ <i18n.Translate>Add</i18n.Translate>
+ </Button>
+ </section>
+ <section>
+ {Object.entries(accountByType).map(([type, list]) => {
+ const Table = tableComponentByAccountType[type as AccountType];
+ return <Table key={type} list={list} onDelete={deleteAccount} />;
+ })}
+ </section>
+ </Fragment>
+ );
+}
+
+function IbanTable({
+ list,
+ onDelete,
+}: {
+ list: KnownBankAccountsInfo[];
+ onDelete: (ac: KnownBankAccountsInfo) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ if (list.length === 0) return <Fragment />;
+ return (
+ <div>
+ <h1>
+ <i18n.Translate>IBAN accounts</i18n.Translate>
+ </h1>
+ <AccountTable>
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Alias</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Bank Id</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Int. Account Number</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Account name</i18n.Translate>
+ </th>
+ <th class="kyc">
+ <i18n.Translate>KYC</i18n.Translate>
+ </th>
+ <th class="actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ {list.map((account) => {
+ const p = account.uri as PaytoUriIBAN;
+ return (
+ <tr key={account.alias}>
+ <td>{account.alias}</td>
+ <td>{p.bic}</td>
+ <td>{p.iban}</td>
+ <td>{p.params["receiver-name"]}</td>
+ <td class="kyc">
+ {account.kyc_completed ? (
+ <SvgIcon
+ title={i18n.str`KYC done`}
+ dangerouslySetInnerHTML={{ __html: checkIcon }}
+ color="green"
+ />
+ ) : (
+ <SvgIcon
+ title={i18n.str`KYC missing`}
+ dangerouslySetInnerHTML={{ __html: warningIcon }}
+ color="orange"
+ />
+ )}
+ </td>
+ <td class="actions">
+ <Button
+ variant="outlined"
+ startIcon={deleteIcon}
+ size="small"
+ onClick={async () => onDelete(account)}
+ color="error"
+ >
+ Forget
+ </Button>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </AccountTable>
+ </div>
+ );
+}
+
+function TalerBankTable({
+ list,
+ onDelete,
+}: {
+ list: KnownBankAccountsInfo[];
+ onDelete: (ac: KnownBankAccountsInfo) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ if (list.length === 0) return <Fragment />;
+ return (
+ <div>
+ <h1>
+ <i18n.Translate>Taler accounts</i18n.Translate>
+ </h1>
+ <AccountTable>
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Alias</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Host</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Account</i18n.Translate>
+ </th>
+ <th class="kyc">
+ <i18n.Translate>KYC</i18n.Translate>
+ </th>
+ <th class="actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ {list.map((account) => {
+ const p = account.uri as PaytoUriTalerBank;
+ return (
+ <tr key={account.alias}>
+ <td>{account.alias}</td>
+ <td>{p.host}</td>
+ <td>{p.account}</td>
+ <td class="kyc">
+ {account.kyc_completed ? (
+ <SvgIcon
+ title={i18n.str`KYC done`}
+ dangerouslySetInnerHTML={{ __html: checkIcon }}
+ color="green"
+ />
+ ) : (
+ <SvgIcon
+ title={i18n.str`KYC missing`}
+ dangerouslySetInnerHTML={{ __html: warningIcon }}
+ color="orange"
+ />
+ )}
+ </td>
+ <td class="actions">
+ <Button
+ variant="outlined"
+ startIcon={deleteIcon}
+ size="small"
+ onClick={async () => onDelete(account)}
+ color="error"
+ >
+ Forget
+ </Button>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </AccountTable>
+ </div>
+ );
+}
+
+function BitcoinTable({
+ list,
+ onDelete,
+}: {
+ list: KnownBankAccountsInfo[];
+ onDelete: (ac: KnownBankAccountsInfo) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ if (list.length === 0) return <Fragment />;
+ return (
+ <div>
+ <h2>
+ <i18n.Translate>Bitcoin accounts</i18n.Translate>
+ </h2>
+ <AccountTable>
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Alias</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Address</i18n.Translate>
+ </th>
+ <th class="kyc">
+ <i18n.Translate>KYC</i18n.Translate>
+ </th>
+ <th class="actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ {list.map((account) => {
+ const p = account.uri as PaytoUriBitcoin;
+ return (
+ <tr key={account.alias}>
+ <td>{account.alias}</td>
+ <td>{p.targetPath}</td>
+ <td class="kyc">
+ {account.kyc_completed ? (
+ <SvgIcon
+ title={i18n.str`KYC done`}
+ dangerouslySetInnerHTML={{ __html: checkIcon }}
+ color="green"
+ />
+ ) : (
+ <SvgIcon
+ title={i18n.str`KYC missing`}
+ dangerouslySetInnerHTML={{ __html: warningIcon }}
+ color="orange"
+ />
+ )}
+ </td>
+ <td class="actions">
+ <Button
+ variant="outlined"
+ startIcon={deleteIcon}
+ size="small"
+ onClick={async () => onDelete(account)}
+ color="error"
+ >
+ Forget
+ </Button>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </AccountTable>
+ </div>
+ );
+}
+
+function BitcoinAddressAccount({ field }: { field: TextFieldHandler }): VNode {
+ const { i18n } = useTranslationContext();
+ const [value, setValue] = useState<string | undefined>(undefined);
+ const errors = undefinedIfEmpty({
+ value: !value ? i18n.str`Can't be empty` : undefined,
+ });
+ return (
+ <Fragment>
+ <h3>
+ <i18n.Translate>Bitcoin Account</i18n.Translate>
+ </h3>
+ <TextField
+ label="Bitcoin address"
+ variant="standard"
+ fullWidth
+ value={value}
+ error={value !== undefined ? errors?.value : undefined}
+ disabled={!field.onInput}
+ onChange={(v) => {
+ setValue(v);
+ if (!errors && field.onInput) {
+ const p = buildPayto("bitcoin", v, undefined);
+ field.onInput(stringifyPaytoUri(p));
+ }
+ }}
+ />
+ </Fragment>
+ );
+}
+
+function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some((k) => (obj as Record<string,unknown>)[k] !== undefined)
+ ? obj
+ : undefined;
+}
+
+function TalerBankAddressAccount({
+ field,
+}: {
+ field: TextFieldHandler;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [host, setHost] = useState<string | undefined>(undefined);
+ const [account, setAccount] = useState<string | undefined>(undefined);
+ const errors = undefinedIfEmpty({
+ host: !host ? i18n.str`Can't be empty` : undefined,
+ account: !account ? i18n.str`Can't be empty` : undefined,
+ });
+ return (
+ <Fragment>
+ <h3>
+ <i18n.Translate>Taler Bank</i18n.Translate>
+ </h3>
+ <TextField
+ label="Bank host"
+ variant="standard"
+ fullWidth
+ value={host}
+ error={host !== undefined ? errors?.host : undefined}
+ disabled={!field.onInput}
+ onChange={(v) => {
+ setHost(v);
+ if (!errors && field.onInput && account) {
+ const p = buildPayto("x-taler-bank", v, account);
+ field.onInput(stringifyPaytoUri(p));
+ }
+ }}
+ />
+ <TextField
+ label="Bank account"
+ variant="standard"
+ fullWidth
+ disabled={!field.onInput}
+ value={account}
+ error={account !== undefined ? errors?.account : undefined}
+ onChange={(v) => {
+ setAccount(v || "");
+ if (!errors && field.onInput && host) {
+ const p = buildPayto("x-taler-bank", host, v);
+ field.onInput(stringifyPaytoUri(p));
+ }
+ }}
+ />
+ </Fragment>
+ );
+}
+
+//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}$/;
+
+function IbanAddressAccount({ field }: { field: TextFieldHandler }): VNode {
+ const { i18n } = useTranslationContext();
+ // const [bic, setBic] = useState<string | undefined>(undefined);
+ const [iban, setIban] = useState<string | undefined>(undefined);
+ const [name, setName] = useState<string | undefined>(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"
+ ? i18n.str`Invalid iban`
+ : undefined,
+ name: !name ? i18n.str`Can't be empty` : undefined,
+ });
+ const errors = errorsFN(iban, name)
+
+ function sendUpdateIfNoErrors(
+ bic: string | undefined,
+ iban: string,
+ name: string,
+ ): void {
+ if (!field.onInput) return;
+ if (!errorsFN(iban, name)) {
+ const p = buildPayto("iban", iban, bic);
+ p.params["receiver-name"] = name;
+ field.onInput(stringifyPaytoUri(p));
+ } else {
+ field.onInput("")
+ }
+ }
+ return (
+ <Fragment>
+ <h3>
+ <i18n.Translate>International Bank Account Number</i18n.Translate>
+ </h3>
+ {/* <p>
+ <TextField
+ label="BIC"
+ variant="filled"
+ placeholder="BANKID"
+ fullWidth
+ value={bic}
+ error={bic !== undefined ? errors?.bic : undefined}
+ disabled={!field.onInput}
+ onChange={(v) => {
+ setBic(v);
+ sendUpdateIfNoErrors(v, iban || "", name || "");
+ }}
+ />
+ </p> */}
+ <p>
+ <TextField
+ label="IBAN"
+ variant="filled"
+ placeholder="XX123456"
+ fullWidth
+ required
+ value={iban}
+ error={iban !== undefined ? errors?.iban : undefined}
+ disabled={!field.onInput}
+ onChange={(v) => {
+ setIban(v);
+ sendUpdateIfNoErrors(bic, v, name || "");
+ }}
+ />
+ </p>
+ <p>
+ <TextField
+ label="Account name"
+ variant="filled"
+ placeholder="Name of the target bank account owner"
+ fullWidth
+ required
+ value={name}
+ error={name !== undefined ? errors?.name : undefined}
+ disabled={!field.onInput}
+ onChange={(v) => {
+ setName(v);
+ sendUpdateIfNoErrors(bic, iban || "", v);
+ }}
+ />
+ </p>
+ </Fragment>
+ );
+}
+
+function CustomFieldByAccountType({
+ type,
+ field,
+}: {
+ type: AccountType;
+ field: TextFieldHandler;
+}): VNode {
+ // const { i18n } = useTranslationContext();
+
+ const AccountForm = formComponentByAccountType[type];
+
+ return (
+ <div>
+ <AccountForm field={field} />
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx
deleted file mode 100644
index dcc0002e6..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-
-import { VNode } from "preact";
-import { useEffect, useRef, useState } from "preact/hooks";
-import { CreateManualWithdraw } from "./CreateManualWithdraw";
-import * as wxApi from '../wxApi'
-import { AcceptManualWithdrawalResult, AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { ReserveCreated } from "./ReserveCreated.js";
-import { route } from 'preact-router';
-import { Pages } from "../NavigationBar.js";
-
-interface Props {
-
-}
-
-export function ManualWithdrawPage({ }: Props): VNode {
- const [success, setSuccess] = useState<AcceptManualWithdrawalResult | undefined>(undefined)
- const [currency, setCurrency] = useState<string | undefined>(undefined)
- const [error, setError] = useState<string | undefined>(undefined)
-
- async function onExchangeChange(exchange: string | undefined) {
- if (!exchange) return
- try {
- const r = await fetch(`${exchange}/keys`)
- const j = await r.json()
- if (j.currency) {
- await wxApi.addExchange({
- exchangeBaseUrl: `${exchange}/`,
- forceUpdate: true
- })
- setCurrency(j.currency)
- }
- } catch (e) {
- setError('The exchange url seems invalid')
- setCurrency(undefined)
- }
- }
-
- async function doCreate(exchangeBaseUrl: string, amount: AmountJson) {
- try {
- const resp = await wxApi.acceptManualWithdrawal(exchangeBaseUrl, Amounts.stringify(amount))
- setSuccess(resp)
- } catch (e) {
- if (e instanceof Error) {
- setError(e.message)
- } else {
- setError('unexpected error')
- }
- setSuccess(undefined)
- }
- }
-
- if (success) {
- return <ReserveCreated reservePub={success.reservePub} paytos={success.exchangePaytoUris} onBack={() => {
- route(Pages.balance)
- }}/>
- }
-
- return <CreateManualWithdraw
- error={error} currency={currency}
- onCreate={doCreate} onExchangeChange={onExchangeChange}
- />;
-}
-
-
-
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts b/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts
new file mode 100644
index 000000000..22b3adb0f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/index.ts
@@ -0,0 +1,61 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { UserAttentionUnreadList } from "@gnu-taler/taler-util";
+import { ErrorAlertView } from "../../components/CurrentAlerts.js";
+import { Loading } from "../../components/Loading.js";
+import { ErrorAlert } from "../../context/alert.js";
+import { compose, StateViewMap } from "../../utils/index.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+
+export type Props = object;
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "error";
+ error: ErrorAlert;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ list: UserAttentionUnreadList;
+ }
+}
+
+const viewMapping: StateViewMap<State> = {
+ loading: Loading,
+ error: ErrorAlertView,
+ ready: ReadyView,
+};
+
+export const NotificationsPage = compose(
+ "NotificationsPage",
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts b/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
new file mode 100644
index 000000000..3ef8250ac
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/state.ts
@@ -0,0 +1,57 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { alertFromError } from "../../context/alert.js";
+import { useBackendContext } from "../../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState(p: Props): State {
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const hook = useAsyncAsHook(async () => {
+ return await api.wallet.call(
+ WalletApiOperation.GetUserAttentionRequests,
+ {},
+ );
+ });
+
+ if (!hook) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ if (hook.hasError) {
+ return {
+ status: "error",
+ error: alertFromError(
+ i18n,
+ i18n.str`Could not load user attention request`,
+ hook,
+ ),
+ };
+ }
+
+ return {
+ status: "ready",
+ error: undefined,
+ list: hook.response.pending,
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx b/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
new file mode 100644
index 000000000..7344f417c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/stories.tsx
@@ -0,0 +1,63 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import {
+ AbsoluteTime,
+ AttentionType,
+ TalerPreciseTimestamp,
+ TransactionIdStr,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "notifications",
+};
+
+export const Ready = tests.createExample(ReadyView, {
+ list: [
+ {
+ when: TalerPreciseTimestamp.now(),
+ read: false,
+ info: {
+ type: AttentionType.KycWithdrawal,
+ transactionId: "123" as TransactionIdStr,
+ },
+ },
+ {
+ when: TalerPreciseTimestamp.now(),
+ read: false,
+ info: {
+ type: AttentionType.MerchantRefund,
+ transactionId: "123" as TransactionIdStr,
+ },
+ },
+ {
+ when: TalerPreciseTimestamp.now(),
+ read: false,
+ info: {
+ type: AttentionType.BackupUnpaid,
+ provider_base_url: "http://sync.taler.net",
+ talerUri: "taler://payment/asdasdasd",
+ },
+ },
+ ],
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/test.ts b/packages/taler-wallet-webextension/src/wallet/Notifications/test.ts
new file mode 100644
index 000000000..c4ce1efc7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/test.ts
@@ -0,0 +1,28 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { expect } from "chai";
+
+describe("Notifications states", () => {
+ it.skip("should create some tests", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
new file mode 100644
index 000000000..03a08016a
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/Notifications/views.tsx
@@ -0,0 +1,211 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AttentionInfo,
+ AttentionType,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import {
+ Column,
+ DateSeparator,
+ HistoryRow,
+ LargeText,
+ SmallLightText,
+} from "../../components/styled/index.js";
+import { Time } from "../../components/Time.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Avatar } from "../../mui/Avatar.js";
+import { Button } from "../../mui/Button.js";
+import { Grid } from "../../mui/Grid.js";
+import { Pages } from "../../NavigationBar.js";
+import { assertUnreachable } from "../../utils/index.js";
+import { State } from "./index.js";
+
+const term = 1000 * 60 * 60 * 24;
+function normalizeToDay(x: number): number {
+ return Math.round(x / term) * term;
+}
+
+export function ReadyView({ list }: State.Ready): VNode {
+ const { i18n } = useTranslationContext();
+ if (list.length < 1) {
+ return (
+ <section>
+ <i18n.Translate>No notification left</i18n.Translate>
+ </section>
+ );
+ }
+
+ const byDate = list.reduce((rv, x) => {
+ const theDate =
+ x.when.t_s === "never" ? 0 : normalizeToDay(x.when.t_s * 1000);
+ if (theDate) {
+ (rv[theDate] = rv[theDate] || []).push(x);
+ }
+
+ return rv;
+ }, {} as { [x: string]: typeof list });
+ const datesWithNotifications = Object.keys(byDate);
+
+ return (
+ <section>
+ {datesWithNotifications.map((d, i) => {
+ return (
+ <Fragment key={i}>
+ <DateSeparator>
+ <Time
+ timestamp={AbsoluteTime.fromMilliseconds(
+ Number.parseInt(d, 10),
+ )}
+ format="dd MMMM yyyy"
+ />
+ </DateSeparator>
+ {byDate[d].map((n, i) => (
+ <NotificationItem
+ key={i}
+ info={n.info}
+ isRead={n.read}
+ timestamp={AbsoluteTime.fromPreciseTimestamp(n.when)}
+ />
+ ))}
+ </Fragment>
+ );
+ })}
+ </section>
+ );
+}
+
+function NotificationItem({
+ info,
+ isRead,
+ timestamp,
+}: {
+ info: AttentionInfo;
+ timestamp: AbsoluteTime;
+ isRead: boolean;
+}): VNode {
+ switch (info.type) {
+ case AttentionType.KycWithdrawal:
+ return (
+ <NotificationLayout
+ timestamp={timestamp}
+ href={Pages.balanceTransaction({ tid: info.transactionId })}
+ title="Withdrawal on hold"
+ subtitle="Know-your-customer validation is required"
+ iconPath={"K"}
+ isRead={isRead}
+ />
+ );
+ case AttentionType.MerchantRefund:
+ return (
+ <NotificationLayout
+ timestamp={timestamp}
+ href={Pages.balanceTransaction({ tid: info.transactionId })}
+ title="Merchant has refund your payment"
+ subtitle="Accept or deny refund"
+ iconPath={"K"}
+ isRead={isRead}
+ />
+ );
+ case AttentionType.BackupUnpaid:
+ return (
+ <NotificationLayout
+ timestamp={timestamp}
+ href={`${Pages.ctaPay}?talerPayUri=${info.talerUri}`}
+ title="Backup provider is unpaid"
+ subtitle="Complete the payment or remove the service provider"
+ iconPath={"K"}
+ isRead={isRead}
+ />
+ );
+ case AttentionType.AuditorDenominationsExpires:
+ return <div>not implemented</div>;
+ case AttentionType.AuditorKeyExpires:
+ return <div>not implemented</div>;
+ case AttentionType.AuditorTosChanged:
+ return <div>not implemented</div>;
+ case AttentionType.ExchangeDenominationsExpired:
+ return <div>not implemented</div>;
+ // case AttentionType.ExchangeDenominationsExpiresSoon:
+ // return <div>not implemented</div>;
+ case AttentionType.ExchangeKeyExpired:
+ return <div>not implemented</div>;
+ // case AttentionType.ExchangeKeyExpiresSoon:
+ // return <div>not implemented</div>;
+ case AttentionType.ExchangeTosChanged:
+ return <div>not implemented</div>;
+ case AttentionType.BackupExpiresSoon:
+ return <div>not implemented</div>;
+ case AttentionType.PushPaymentReceived:
+ return <div>not implemented</div>;
+ case AttentionType.PullPaymentPaid:
+ return <div>not implemented</div>;
+ default:
+ assertUnreachable(info);
+ }
+}
+
+function NotificationLayout(props: {
+ title: string;
+ href: string;
+ subtitle?: string;
+ timestamp: AbsoluteTime;
+ iconPath: string;
+ isRead: boolean;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <HistoryRow
+ href={props.href}
+ style={{
+ backgroundColor: props.isRead ? "lightcyan" : "inherit",
+ alignItems: "center",
+ }}
+ >
+ <Avatar
+ style={{
+ border: "solid gray 1px",
+ color: "gray",
+ boxSizing: "border-box",
+ }}
+ >
+ {props.iconPath}
+ </Avatar>
+ <Column>
+ <LargeText>
+ <div>{props.title}</div>
+ {props.subtitle && (
+ <div style={{ color: "gray", fontSize: "medium", marginTop: 5 }}>
+ {props.subtitle}
+ </div>
+ )}
+ </LargeText>
+ <SmallLightText style={{ marginTop: 5 }}>
+ <Time timestamp={props.timestamp} format="HH:mm" />
+ </SmallLightText>
+ </Column>
+ <Column>
+ <Grid>
+ <Button variant="outlined">
+ <i18n.Translate>Ignore</i18n.Translate>
+ </Button>
+ </Grid>
+ </Column>
+ </HistoryRow>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
index d1e76c053..e5c43e230 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,38 +15,37 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { ConfirmProviderView as TestedComponent } from './ProviderAddPage';
+import * as tests from "@gnu-taler/web-util/testing";
+import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage.js";
export default {
- title: 'wallet/backup/confirm',
+ title: "confirm",
component: TestedComponent,
argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
+ onRetry: { action: "onRetry" },
+ onDelete: { action: "onDelete" },
+ onBack: { action: "onBack" },
+ },
};
-
-export const DemoService = createExample(TestedComponent, {
- url: 'https://sync.demo.taler.net/',
+export const DemoService = tests.createExample(TestedComponent, {
+ url: "https://sync.demo.taler.net/",
provider: {
- annual_fee: 'KUDOS:0.1',
- storage_limit_in_megabytes: 20,
- supported_protocol_version: '1'
- }
+ annual_fee: "KUDOS:0.1",
+ storage_limit_in_megabytes: 20,
+ supported_protocol_version: "1",
+ },
});
-export const FreeService = createExample(TestedComponent, {
- url: 'https://sync.taler:9667/',
+export const FreeService = tests.createExample(TestedComponent, {
+ url: "https://sync.taler:9667/",
provider: {
- annual_fee: 'ARS:0',
- storage_limit_in_megabytes: 20,
- supported_protocol_version: '1'
- }
+ annual_fee: "ARS:0",
+ storage_limit_in_megabytes: 20,
+ supported_protocol_version: "1",
+ },
});
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
index 1c7fdc829..6ade0718a 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -16,43 +16,34 @@
import {
Amounts,
- BackupBackupProviderTerms,
canonicalizeBaseUrl,
- i18n,
} from "@gnu-taler/taler-util";
-import { VNode, h } from "preact";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
-import { Checkbox } from "../components/Checkbox";
-import { ErrorMessage } from "../components/ErrorMessage";
+import { Checkbox } from "../components/Checkbox.js";
+import { ErrorMessage } from "../components/ErrorMessage.js";
import {
- Button,
- ButtonPrimary,
Input,
LightText,
- WalletBox,
SmallLightText,
-} from "../components/styled/index";
-import * as wxApi from "../wxApi";
+ SubTitle,
+ Title,
+} from "../components/styled/index.js";
+import { useBackendContext } from "../context/backend.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Button } from "../mui/Button.js";
+import { queryToSlashConfig } from "../utils/index.js";
interface Props {
currency: string;
- onBack: () => void;
+ onBack: () => Promise<void>;
}
-function getJsonIfOk(r: Response) {
- if (r.ok) {
- return r.json();
- } else {
- if (r.status >= 400 && r.status < 500) {
- throw new Error(`URL may not be right: (${r.status}) ${r.statusText}`);
- } else {
- throw new Error(
- `Try another server: (${r.status}) ${
- r.statusText || "internal server error"
- }`,
- );
- }
- }
+interface BackupBackupProviderTerms {
+ annual_fee: string;
+ storage_limit_in_megabytes: number;
+ supported_protocol_version: string;
}
export function ProviderAddPage({ onBack }: Props): VNode {
@@ -60,24 +51,14 @@ export function ProviderAddPage({ onBack }: Props): VNode {
| { url: string; name: string; provider: BackupBackupProviderTerms }
| undefined
>(undefined);
-
- async function getProviderInfo(
- url: string,
- ): Promise<BackupBackupProviderTerms> {
- return fetch(`${url}config`)
- .catch((e) => {
- throw new Error(`Network error`);
- })
- .then(getJsonIfOk);
- }
-
+ const api = useBackendContext();
if (!verifying) {
return (
<SetUrlView
onCancel={onBack}
- onVerify={(url) => getProviderInfo(url)}
+ onVerify={(url) => queryToSlashConfig(url)}
onConfirm={(url, name) =>
- getProviderInfo(url)
+ queryToSlashConfig<BackupBackupProviderTerms>(url)
.then((provider) => {
setVerifying({ url, name, provider });
})
@@ -90,11 +71,17 @@ export function ProviderAddPage({ onBack }: Props): VNode {
<ConfirmProviderView
provider={verifying.provider}
url={verifying.url}
- onCancel={() => {
+ onCancel={async () => {
setVerifying(undefined);
}}
onConfirm={() => {
- wxApi.addBackupProvider(verifying.url, verifying.name).then(onBack);
+ return api.wallet
+ .call(WalletApiOperation.AddBackupProvider, {
+ backupProviderBaseUrl: verifying.url,
+ name: verifying.name,
+ activate: true,
+ })
+ .then(onBack);
}}
/>
);
@@ -102,7 +89,7 @@ export function ProviderAddPage({ onBack }: Props): VNode {
export interface SetUrlViewProps {
initialValue?: string;
- onCancel: () => void;
+ onCancel: () => Promise<void>;
onVerify: (s: string) => Promise<BackupBackupProviderTerms | undefined>;
onConfirm: (url: string, name: string) => Promise<string | undefined>;
withError?: string;
@@ -114,7 +101,8 @@ export function SetUrlView({
onVerify,
onConfirm,
withError,
-}: SetUrlViewProps) {
+}: SetUrlViewProps): VNode {
+ const { i18n } = useTranslationContext();
const [value, setValue] = useState<string>(initialValue || "");
const [urlError, setUrlError] = useState(false);
const [name, setName] = useState<string | undefined>(undefined);
@@ -135,19 +123,29 @@ export function SetUrlView({
setUrlError(true);
setName(undefined);
}
- }, [value]);
+ }, [onVerify, value]);
return (
- <WalletBox>
+ <Fragment>
<section>
- <h1> Add backup provider</h1>
- <ErrorMessage
- title={error && "Could not get provider information"}
- description={error}
- />
- <LightText> Backup providers may charge for their service</LightText>
+ <Title>
+ <i18n.Translate>Add backup provider</i18n.Translate>
+ </Title>
+ {error && (
+ <ErrorMessage
+ title={i18n.str`Could not get provider information`}
+ description={error}
+ />
+ )}
+ <LightText>
+ <i18n.Translate>
+ Backup providers may charge for their service
+ </i18n.Translate>
+ </LightText>
<p>
<Input invalid={urlError}>
- <label>URL</label>
+ <label>
+ <i18n.Translate>URL</i18n.Translate>
+ </label>
<input
type="text"
placeholder="https://"
@@ -156,7 +154,9 @@ export function SetUrlView({
/>
</Input>
<Input>
- <label>Name</label>
+ <label>
+ <i18n.Translate>Name</i18n.Translate>
+ </label>
<input
type="text"
disabled={name === undefined}
@@ -167,10 +167,11 @@ export function SetUrlView({
</p>
</section>
<footer>
- <Button onClick={onCancel}>
- <i18n.Translate> &lt; Back</i18n.Translate>
+ <Button variant="contained" color="secondary" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
</Button>
- <ButtonPrimary
+ <Button
+ variant="contained"
disabled={!value && !urlError}
onClick={() => {
const url = canonicalizeBaseUrl(value);
@@ -180,65 +181,80 @@ export function SetUrlView({
}}
>
<i18n.Translate>Next</i18n.Translate>
- </ButtonPrimary>
+ </Button>
</footer>
- </WalletBox>
+ </Fragment>
);
}
export interface ConfirmProviderViewProps {
provider: BackupBackupProviderTerms;
url: string;
- onCancel: () => void;
- onConfirm: () => void;
+ onCancel: () => Promise<void>;
+ onConfirm: () => Promise<void>;
}
export function ConfirmProviderView({
url,
provider,
onCancel,
onConfirm,
-}: ConfirmProviderViewProps) {
+}: ConfirmProviderViewProps): VNode {
const [accepted, setAccepted] = useState(false);
+ const { i18n } = useTranslationContext();
return (
- <WalletBox>
+ <Fragment>
<section>
- <h1>Review terms of service</h1>
+ <Title>
+ <i18n.Translate>Review terms of service</i18n.Translate>
+ </Title>
<div>
- Provider URL:{" "}
- <a href={url} target="_blank">
+ <i18n.Translate>Provider URL</i18n.Translate>:{" "}
+ <a href={url} target="_blank" rel="noreferrer">
{url}
</a>
</div>
<SmallLightText>
- Please review and accept this provider's terms of service
+ <i18n.Translate>
+ Please review and accept this provider&apos;s terms of service
+ </i18n.Translate>
</SmallLightText>
- <h2>1. Pricing</h2>
+ <SubTitle>
+ 1. <i18n.Translate>Pricing</i18n.Translate>
+ </SubTitle>
<p>
- {Amounts.isZero(provider.annual_fee)
- ? "free of charge"
- : `${provider.annual_fee} per year of service`}
+ {Amounts.isZero(provider.annual_fee) ? (
+ i18n.str`free of charge`
+ ) : (
+ <i18n.Translate>
+ {provider.annual_fee} per year of service
+ </i18n.Translate>
+ )}
</p>
- <h2>2. Storage</h2>
+ <SubTitle>
+ 2. <i18n.Translate>Storage</i18n.Translate>
+ </SubTitle>
<p>
- {provider.storage_limit_in_megabytes} megabytes of storage per year of
- service
+ <i18n.Translate>
+ {provider.storage_limit_in_megabytes} megabytes of storage per year
+ of service
+ </i18n.Translate>
</p>
<Checkbox
- label="Accept terms of service"
+ label={i18n.str`Accept terms of service`}
name="terms"
- onToggle={() => setAccepted((old) => !old)}
+ onToggle={async () => setAccepted((old) => !old)}
enabled={accepted}
/>
</section>
<footer>
- <Button onClick={onCancel}>
- <i18n.Translate> &lt; Back</i18n.Translate>
+ <Button variant="contained" color="secondary" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
</Button>
- <ButtonPrimary disabled={!accepted} onClick={onConfirm}>
+ <Button variant="contained" disabled={!accepted} onClick={onConfirm}>
<i18n.Translate>Add provider</i18n.Translate>
- </ButtonPrimary>
+ </Button>
</footer>
- </WalletBox>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
index 4890e5e9c..d35a0ff99 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,39 +15,37 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { SetUrlView as TestedComponent } from './ProviderAddPage';
+import * as tests from "@gnu-taler/web-util/testing";
+import { SetUrlView as TestedComponent } from "./ProviderAddPage.js";
export default {
- title: 'wallet/backup/add',
+ title: "add",
component: TestedComponent,
argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
+ onRetry: { action: "onRetry" },
+ onDelete: { action: "onDelete" },
+ onBack: { action: "onBack" },
+ },
};
+export const Initial = tests.createExample(TestedComponent, {});
-export const Initial = createExample(TestedComponent, {
-});
-
-export const WithValue = createExample(TestedComponent, {
- initialValue: 'sync.demo.taler.net'
-});
+export const WithValue = tests.createExample(TestedComponent, {
+ initialValue: "sync.demo.taler.net",
+});
-export const WithConnectionError = createExample(TestedComponent, {
- withError: 'Network error'
-});
+export const WithConnectionError = tests.createExample(TestedComponent, {
+ withError: "Network error",
+});
-export const WithClientError = createExample(TestedComponent, {
- withError: 'URL may not be right: (404) Not Found'
-});
+export const WithClientError = tests.createExample(TestedComponent, {
+ withError: "URL may not be right: (404) Not Found",
+});
-export const WithServerError = createExample(TestedComponent, {
- withError: 'Try another server: (500) Internal Server Error'
-});
+export const WithServerError = tests.createExample(TestedComponent, {
+ withError: "Try another server: (500) Internal Server Error",
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
index 67ff83442..d4ee09b89 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,224 +15,218 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core';
-import { createExample } from '../test-utils';
-import { ProviderView as TestedComponent } from './ProviderDetailPage';
+import {
+ AbsoluteTime,
+ AmountString,
+ ProviderPaymentType,
+ TalerPreciseTimestamp,
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { ProviderView as TestedComponent } from "./ProviderDetailPage.js";
export default {
- title: 'wallet/backup/details',
+ title: "provider details",
component: TestedComponent,
argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
+ onRetry: { action: "onRetry" },
+ onDelete: { action: "onDelete" },
+ onBack: { action: "onBack" },
+ },
};
-
-export const Active = createExample(TestedComponent, {
+export const Active = tests.createExample(TestedComponent, {
info: {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp:
+ TalerPreciseTimestamp.fromSeconds(1625063925),
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
+ },
+ terms: {
+ annualFee: "EUR:1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
+ },
});
-export const ActiveErrorSync = createExample(TestedComponent, {
+export const ActiveErrorSync = tests.createExample(TestedComponent, {
info: {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- lastAttemptedBackupTimestamp: {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp:
+ TalerPreciseTimestamp.fromSeconds(1625063925),
+ lastAttemptedBackupTimestamp:
+ TalerPreciseTimestamp.fromSeconds(1625063925078),
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
},
lastError: {
code: 2002,
- details: 'details',
- hint: 'error hint from the server',
- message: 'message'
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
+ details: "details",
+ when: AbsoluteTime.now(),
+ hint: "error hint from the server",
+ message: "message",
+ },
+ terms: {
+ annualFee: "EUR:1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
+ },
});
-export const ActiveBackupProblemUnreadable = createExample(TestedComponent, {
- info: {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
- ],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
+export const ActiveBackupProblemUnreadable = tests.createExample(
+ TestedComponent,
+ {
+ info: {
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp:
+ TalerPreciseTimestamp.fromSeconds(1625063925),
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
+ ],
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
+ },
+ backupProblem: {
+ type: "backup-unreadable",
+ },
+ terms: {
+ annualFee: "EUR:1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
},
- backupProblem: {
- type: 'backup-unreadable'
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
-});
+ },
+);
-export const ActiveBackupProblemDevice = createExample(TestedComponent, {
+export const ActiveBackupProblemDevice = tests.createExample(TestedComponent, {
info: {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.taler:9967/",
- "lastSuccessfulBackupTimestamp": {
- "t_ms": 1625063925078
- },
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp:
+ TalerPreciseTimestamp.fromSeconds(1625063925078),
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
},
backupProblem: {
- type: 'backup-conflicting-device',
- myDeviceId: 'my-device-id',
- otherDeviceId: 'other-device-id',
- backupTimestamp: {
- "t_ms": 1656599921000
- }
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
+ type: "backup-conflicting-device",
+ myDeviceId: "my-device-id",
+ otherDeviceId: "other-device-id",
+ backupTimestamp: AbsoluteTime.fromMilliseconds(1656599921000),
+ },
+ terms: {
+ annualFee: "EUR:1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
+ },
});
-export const InactiveUnpaid = createExample(TestedComponent, {
+export const InactiveUnpaid = tests.createExample(TestedComponent, {
info: {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.Unpaid,
- },
- "terms": {
- "annualFee": "EUR:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
+ active: false,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.Unpaid,
+ },
+ terms: {
+ annualFee: "EUR:0.1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
+ },
});
-export const InactiveInsufficientBalance = createExample(TestedComponent, {
- info: {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.InsufficientBalance,
- },
- "terms": {
- "annualFee": "EUR:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
-});
+export const InactiveInsufficientBalance = tests.createExample(
+ TestedComponent,
+ {
+ info: {
+ active: false,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.InsufficientBalance,
+ amount: "EUR:123" as AmountString,
+ },
+ terms: {
+ annualFee: "EUR:0.1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
+ },
+ },
+);
-export const InactivePending = createExample(TestedComponent, {
+export const InactivePending = tests.createExample(TestedComponent, {
info: {
- "active": false,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.Pending,
- },
- "terms": {
- "annualFee": "EUR:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
+ active: false,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.Pending,
+ talerUri: "taler://pay/sad",
+ },
+ terms: {
+ annualFee: "EUR:0.1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
+ },
});
-
-export const ActiveTermsChanged = createExample(TestedComponent, {
+export const ActiveTermsChanged = tests.createExample(TestedComponent, {
info: {
- "active": true,
- name:'sync.demo',
- "syncProviderBaseUrl": "http://sync.demo.taler.net/",
- "paymentProposalIds": [],
- "paymentStatus": {
- "type": ProviderPaymentType.TermsChanged,
- paidUntil: {
- t_ms: 1656599921000
- },
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.TermsChanged,
+ paidUntil: AbsoluteTime.fromMilliseconds(1656599921000),
newTerms: {
- "annualFee": "EUR:10",
- "storageLimitInMegabytes": 8,
- "supportedProtocolVersion": "0.0"
+ annualFee: "EUR:10" as AmountString,
+ storageLimitInMegabytes: 8,
+ supportedProtocolVersion: "0.0",
},
oldTerms: {
- "annualFee": "EUR:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- },
- "terms": {
- "annualFee": "EUR:0.1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
+ annualFee: "EUR:0.1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
+ },
+ terms: {
+ annualFee: "EUR:0.1" as AmountString,
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "0.0",
+ },
+ },
});
-
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
index c45458eb7..d628b68e8 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
@@ -1,195 +1,328 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
-
-import { i18n, Timestamp } from "@gnu-taler/taler-util";
-import { ProviderInfo, ProviderPaymentStatus, ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
-import { format, formatDuration, intervalToDuration } from "date-fns";
+import * as utils from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ ProviderInfo,
+ ProviderPaymentStatus,
+ ProviderPaymentType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { ErrorMessage } from "../components/ErrorMessage";
-import { Button, ButtonDestructive, ButtonPrimary, PaymentStatus, WalletBox, SmallLightText } from "../components/styled";
-import { useProviderStatus } from "../hooks/useProviderStatus";
+import { ErrorAlertView } from "../components/CurrentAlerts.js";
+import { ErrorMessage } from "../components/ErrorMessage.js";
+import { Loading } from "../components/Loading.js";
+import { Time } from "../components/Time.js";
+import { PaymentStatus, SmallLightText } from "../components/styled/index.js";
+import { alertFromError } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { Button } from "../mui/Button.js";
interface Props {
pid: string;
- onBack: () => void;
+ onBack: () => Promise<void>;
+ onPayProvider: (uri: string) => Promise<void>;
+ onWithdraw: (amount: string) => Promise<void>;
}
-export function ProviderDetailPage({ pid, onBack }: Props): VNode {
- const status = useProviderStatus(pid)
- if (!status) {
- return <div><i18n.Translate>Loading...</i18n.Translate></div>
+export function ProviderDetailPage({
+ pid: providerURL,
+ onBack,
+ onPayProvider,
+ onWithdraw,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+ async function getProviderInfo(): Promise<ProviderInfo | null> {
+ //create a first list of backup info by currency
+ const status = await api.wallet.call(WalletApiOperation.GetBackupInfo, {});
+
+ const providers = status.providers.filter(
+ (p) => p.syncProviderBaseUrl === providerURL,
+ );
+ return providers.length ? providers[0] : null;
}
- if (!status.info) {
- onBack()
- return <div />
+
+ const state = useAsyncAsHook(getProviderInfo);
+
+ if (!state) {
+ return <Loading />;
}
- return <ProviderView info={status.info}
- onSync={status.sync}
- onDelete={() => status.remove().then(onBack)}
- onBack={onBack}
- onExtend={() => { null }}
- />;
+ if (state.hasError) {
+ return (
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`There was an error loading the provider detail for &quot;${providerURL}&quot;`,
+ state,
+ )}
+ />
+ );
+ }
+ const info = state.response;
+ if (info === null) {
+ return (
+ <Fragment>
+ <section>
+ <p>
+ <i18n.Translate>
+ There is not known provider with url &quot;{providerURL}&quot;.
+ </i18n.Translate>
+ </p>
+ </section>
+ <footer>
+ <Button variant="contained" color="secondary" onClick={onBack}>
+ <i18n.Translate>See providers</i18n.Translate>
+ </Button>
+ <div />
+ </footer>
+ </Fragment>
+ );
+ }
+
+ return (
+ <ProviderView
+ info={info}
+ onSync={async () =>
+ api.wallet
+ .call(WalletApiOperation.RunBackupCycle, {
+ providers: [providerURL],
+ })
+ .then()
+ }
+ onPayProvider={async () => {
+ if (info.paymentStatus.type !== ProviderPaymentType.Pending) return;
+ if (!info.paymentStatus.talerUri) return;
+ onPayProvider(info.paymentStatus.talerUri);
+ }}
+ onWithdraw={async () => {
+ if (info.paymentStatus.type !== ProviderPaymentType.InsufficientBalance)
+ return;
+ onWithdraw(info.paymentStatus.amount);
+ }}
+ onDelete={() =>
+ api.wallet
+ .call(WalletApiOperation.RemoveBackupProvider, {
+ provider: providerURL,
+ })
+ .then(onBack)
+ }
+ onBack={onBack}
+ onExtend={async () => {
+ null;
+ }}
+ />
+ );
}
export interface ViewProps {
info: ProviderInfo;
- onDelete: () => void;
- onSync: () => void;
- onBack: () => void;
- onExtend: () => void;
+ onDelete: () => Promise<void>;
+ onSync: () => Promise<void>;
+ onBack: () => Promise<void>;
+ onExtend: () => Promise<void>;
+ onPayProvider: () => Promise<void>;
+ onWithdraw: () => Promise<void>;
}
-export function ProviderView({ info, onDelete, onSync, onBack, onExtend }: ViewProps): VNode {
- const lb = info?.lastSuccessfulBackupTimestamp
- const isPaid = info.paymentStatus.type === ProviderPaymentType.Paid || info.paymentStatus.type === ProviderPaymentType.TermsChanged
+export function ProviderView({
+ info,
+ onDelete,
+ onPayProvider,
+ onWithdraw,
+ onSync,
+ onBack,
+ onExtend,
+}: ViewProps): VNode {
+ const { i18n } = useTranslationContext();
+ const lb = info.lastSuccessfulBackupTimestamp
+ ? AbsoluteTime.fromPreciseTimestamp(info.lastSuccessfulBackupTimestamp)
+ : undefined;
+ const isPaid =
+ info.paymentStatus.type === ProviderPaymentType.Paid ||
+ info.paymentStatus.type === ProviderPaymentType.TermsChanged;
return (
- <WalletBox>
+ <Fragment>
<Error info={info} />
<header>
- <h3>{info.name} <SmallLightText>{info.syncProviderBaseUrl}</SmallLightText></h3>
- <PaymentStatus color={isPaid ? 'rgb(28, 184, 65)' : 'rgb(202, 60, 60)'}>{isPaid ? 'Paid' : 'Unpaid'}</PaymentStatus>
+ <h3>
+ {info.name}{" "}
+ <SmallLightText>{info.syncProviderBaseUrl}</SmallLightText>
+ </h3>
+ <PaymentStatus color={isPaid ? "rgb(28, 184, 65)" : "rgb(202, 60, 60)"}>
+ {isPaid ? "Paid" : "Unpaid"}
+ </PaymentStatus>
</header>
<section>
- <p><b>Last backup:</b> {lb == null || lb.t_ms == "never" ? "never" : format(lb.t_ms, 'dd MMM yyyy')} </p>
- <ButtonPrimary onClick={onSync}><i18n.Translate>Back up</i18n.Translate></ButtonPrimary>
- {info.terms && <Fragment>
- <p><b>Provider fee:</b> {info.terms && info.terms.annualFee} per year</p>
- </Fragment>
- }
- <p>{descriptionByStatus(info.paymentStatus)}</p>
- <ButtonPrimary disabled onClick={onExtend}><i18n.Translate>Extend</i18n.Translate></ButtonPrimary>
-
- {info.paymentStatus.type === ProviderPaymentType.TermsChanged && <div>
- <p><i18n.Translate>terms has changed, extending the service will imply accepting the new terms of service</i18n.Translate></p>
- <table>
- <thead>
- <tr>
- <td></td>
- <td><i18n.Translate>old</i18n.Translate></td>
- <td> -&gt;</td>
- <td><i18n.Translate>new</i18n.Translate></td>
- </tr>
- </thead>
- <tbody>
-
- <tr>
- <td><i18n.Translate>fee</i18n.Translate></td>
- <td>{info.paymentStatus.oldTerms.annualFee}</td>
- <td>-&gt;</td>
- <td>{info.paymentStatus.newTerms.annualFee}</td>
- </tr>
- <tr>
- <td><i18n.Translate>storage</i18n.Translate></td>
- <td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
- <td>-&gt;</td>
- <td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
- </tr>
- </tbody>
- </table>
- </div>}
+ <p>
+ <b>
+ <i18n.Translate>Last backup</i18n.Translate>:
+ </b>{" "}
+ <Time timestamp={lb} format="dd MMMM yyyy" />
+ </p>
+ <Button variant="contained" onClick={onSync}>
+ <i18n.Translate>Back up</i18n.Translate>
+ </Button>
+ {info.terms && (
+ <Fragment>
+ <p>
+ <b>
+ <i18n.Translate>Provider fee</i18n.Translate>:
+ </b>{" "}
+ {info.terms && info.terms.annualFee}{" "}
+ <i18n.Translate>per year</i18n.Translate>
+ </p>
+ </Fragment>
+ )}
+ <p>{descriptionByStatus(info.paymentStatus, i18n)}</p>
+ <Button variant="contained" disabled onClick={onExtend}>
+ <i18n.Translate>Extend</i18n.Translate>
+ </Button>
+ {info.paymentStatus.type === ProviderPaymentType.TermsChanged && (
+ <div>
+ <p>
+ <i18n.Translate>
+ terms has changed, extending the service will imply accepting
+ the new terms of service
+ </i18n.Translate>
+ </p>
+ <table>
+ <thead>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <i18n.Translate>old</i18n.Translate>
+ </td>
+ <td> -&gt;</td>
+ <td>
+ <i18n.Translate>new</i18n.Translate>
+ </td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <i18n.Translate>fee</i18n.Translate>
+ </td>
+ <td>{info.paymentStatus.oldTerms.annualFee}</td>
+ <td>-&gt;</td>
+ <td>{info.paymentStatus.newTerms.annualFee}</td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>storage</i18n.Translate>
+ </td>
+ <td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td>
+ <td>-&gt;</td>
+ <td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ )}
</section>
<footer>
- <Button onClick={onBack}><i18n.Translate> &lt; back</i18n.Translate></Button>
+ <Button variant="contained" color="secondary" onClick={onBack}>
+ <i18n.Translate>See providers</i18n.Translate>
+ </Button>
<div>
- <ButtonDestructive onClick={onDelete}><i18n.Translate>remove provider</i18n.Translate></ButtonDestructive>
+ <Button variant="contained" color="error" onClick={onDelete}>
+ <i18n.Translate>Remove provider</i18n.Translate>
+ </Button>
+ {info.paymentStatus.type === ProviderPaymentType.Pending &&
+ info.paymentStatus.talerUri ? (
+ <Button variant="contained" color="primary" onClick={onPayProvider}>
+ <i18n.Translate>Pay</i18n.Translate>
+ </Button>
+ ) : undefined}
+ {info.paymentStatus.type ===
+ ProviderPaymentType.InsufficientBalance ? (
+ <Button variant="contained" color="primary" onClick={onWithdraw}>
+ <i18n.Translate>Withdraw</i18n.Translate>
+ </Button>
+ ) : undefined}
</div>
</footer>
- </WalletBox>
- )
-}
-
-function daysSince(d?: Timestamp) {
- if (!d || d.t_ms === 'never') return 'never synced'
- const duration = intervalToDuration({
- start: d.t_ms,
- end: new Date(),
- })
- const str = formatDuration(duration, {
- delimiter: ', ',
- format: [
- duration?.years ? i18n.str`years` : (
- duration?.months ? i18n.str`months` : (
- duration?.days ? i18n.str`days` : (
- duration?.hours ? i18n.str`hours` : (
- duration?.minutes ? i18n.str`minutes` : i18n.str`seconds`
- )
- )
- )
- )
- ]
- })
- return `synced ${str} ago`
+ </Fragment>
+ );
}
-function Error({ info }: { info: ProviderInfo }) {
+function Error({ info }: { info: ProviderInfo }): VNode {
+ const { i18n } = useTranslationContext();
if (info.lastError) {
- return <ErrorMessage title={info.lastError.hint} />
+ return (
+ <ErrorMessage
+ title={i18n.str`This provider has reported an error`}
+ description={info.lastError.hint}
+ />
+ );
}
if (info.backupProblem) {
switch (info.backupProblem.type) {
case "backup-conflicting-device":
- return <ErrorMessage title={<Fragment>
- <i18n.Translate>There is conflict with another backup from <b>{info.backupProblem.otherDeviceId}</b></i18n.Translate>
- </Fragment>} />
+ return (
+ <ErrorMessage
+ title={i18n.str`There is conflict with another backup from &quot;${info.backupProblem.otherDeviceId}&quot;`}
+ />
+ );
case "backup-unreadable":
- return <ErrorMessage title="Backup is not readable" />
+ return <ErrorMessage title={i18n.str`Backup is not readable`} />;
default:
- return <ErrorMessage title={<Fragment>
- <i18n.Translate>Unknown backup problem: {JSON.stringify(info.backupProblem)}</i18n.Translate>
- </Fragment>} />
+ return (
+ <ErrorMessage
+ title={i18n.str`Unknown backup problem: ${JSON.stringify(
+ info.backupProblem,
+ )}`}
+ />
+ );
}
}
- return null
+ return <Fragment />;
}
-function colorByStatus(status: ProviderPaymentType) {
- switch (status) {
- case ProviderPaymentType.InsufficientBalance:
- return 'rgb(223, 117, 20)'
- case ProviderPaymentType.Unpaid:
- return 'rgb(202, 60, 60)'
- case ProviderPaymentType.Paid:
- return 'rgb(28, 184, 65)'
- case ProviderPaymentType.Pending:
- return 'gray'
- case ProviderPaymentType.InsufficientBalance:
- return 'rgb(202, 60, 60)'
- case ProviderPaymentType.TermsChanged:
- return 'rgb(202, 60, 60)'
- }
-}
-
-function descriptionByStatus(status: ProviderPaymentStatus) {
+function descriptionByStatus(
+ status: ProviderPaymentStatus,
+ i18n: typeof utils.i18n,
+): VNode {
switch (status.type) {
- // return i18n.str`no enough balance to make the payment`
- // return i18n.str`not paid yet`
case ProviderPaymentType.Paid:
case ProviderPaymentType.TermsChanged:
- if (status.paidUntil.t_ms === 'never') {
- return i18n.str`service paid`
- } else {
- return <Fragment>
- <b>Backup valid until:</b> {format(status.paidUntil.t_ms, 'dd MMM yyyy')}
- </Fragment>
+ if (status.paidUntil.t_ms === "never") {
+ return (
+ <span>
+ <i18n.Translate>service paid</i18n.Translate>
+ </span>
+ );
}
+ return (
+ <Fragment>
+ <b>
+ <i18n.Translate>Backup valid until</i18n.Translate>:
+ </b>{" "}
+ <Time timestamp={status.paidUntil} format="dd MMM yyyy" />
+ </Fragment>
+ );
+
case ProviderPaymentType.Unpaid:
case ProviderPaymentType.InsufficientBalance:
case ProviderPaymentType.Pending:
- return ''
+ return <span />;
}
}
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx
new file mode 100644
index 000000000..8fc6985b0
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx
@@ -0,0 +1,29 @@
+/*
+ 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 { QrReaderPage } from "./QrReader.js";
+
+export default {
+ title: "qr reader",
+};
+
+export const Reading = tests.createExample(QrReaderPage, {});
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
new file mode 100644
index 000000000..a01ea6967
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
@@ -0,0 +1,392 @@
+/*
+ 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 {
+ 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 { h, VNode } from "preact";
+import { useRef, useState } from "preact/hooks";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
+import { Alert } from "../mui/Alert.js";
+import { Button } from "../mui/Button.js";
+import { Grid } from "../mui/Grid.js";
+import { InputFile } from "../mui/InputFile.js";
+import { TextField } from "../mui/TextField.js";
+
+const QrCanvas = css`
+ width: 80%;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 8px;
+ background-color: black;
+`;
+
+const LINE_COLOR = "#FF3B58";
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ & > * {
+ margin-bottom: 20px;
+ }
+`;
+
+export interface Props {
+ onDetected: (url: TalerUri) => void;
+}
+
+type XY = { x: number; y: number };
+
+function drawLine(
+ canvas: CanvasRenderingContext2D,
+ begin: XY,
+ end: XY,
+ color: string,
+) {
+ canvas.beginPath();
+ canvas.moveTo(begin.x, begin.y);
+ canvas.lineTo(end.x, end.y);
+ canvas.lineWidth = 4;
+ canvas.strokeStyle = color;
+ canvas.stroke();
+}
+
+function drawBox(context: CanvasRenderingContext2D, code: pr.QRCode) {
+ drawLine(
+ context,
+ code.location.topLeftCorner,
+ code.location.topRightCorner,
+ LINE_COLOR,
+ );
+ drawLine(
+ context,
+ code.location.topRightCorner,
+ code.location.bottomRightCorner,
+ LINE_COLOR,
+ );
+ drawLine(
+ context,
+ code.location.bottomRightCorner,
+ code.location.bottomLeftCorner,
+ LINE_COLOR,
+ );
+ drawLine(
+ context,
+ code.location.bottomLeftCorner,
+ code.location.topLeftCorner,
+ LINE_COLOR,
+ );
+}
+
+const SCAN_PER_SECONDS = 3;
+const TIME_BETWEEN_FRAMES = 1000 / SCAN_PER_SECONDS;
+
+async function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function drawIntoCanvasAndGetQR(
+ tag: HTMLVideoElement | HTMLImageElement,
+ canvas: HTMLCanvasElement,
+): string | undefined {
+ const context = canvas.getContext("2d");
+ if (!context) {
+ throw Error("no 2d canvas context");
+ }
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ context.drawImage(tag, 0, 0, canvas.width, canvas.height);
+ const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
+ const code = jsQR.default(imgData.data, canvas.width, canvas.height, {
+ inversionAttempts: "attemptBoth",
+ });
+ if (code) {
+ drawBox(context, code);
+ return code.data;
+ }
+ return undefined;
+}
+
+async function readNextFrame(
+ video: HTMLVideoElement,
+ canvas: HTMLCanvasElement,
+): Promise<string | undefined> {
+ const requestFrame =
+ "requestVideoFrameCallback" in video
+ ? video.requestVideoFrameCallback.bind(video)
+ : requestAnimationFrame;
+
+ return new Promise<string | undefined>((ok, bad) => {
+ requestFrame(() => {
+ try {
+ const code = drawIntoCanvasAndGetQR(video, canvas);
+ ok(code);
+ } catch (error) {
+ bad(error);
+ }
+ });
+ });
+}
+
+async function createCanvasFromVideo(
+ video: HTMLVideoElement,
+ canvas: HTMLCanvasElement,
+): Promise<string> {
+ const context = canvas.getContext("2d", {
+ willReadFrequently: true,
+ });
+ if (!context) {
+ throw Error("no 2d canvas context");
+ }
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+
+ let last = Date.now();
+
+ let found: string | undefined = undefined;
+ while (!found) {
+ const timeSinceLast = Date.now() - last;
+ if (timeSinceLast < TIME_BETWEEN_FRAMES) {
+ await delay(TIME_BETWEEN_FRAMES - timeSinceLast);
+ }
+ last = Date.now();
+ found = await readNextFrame(video, canvas);
+ }
+ video.pause();
+ return found;
+}
+
+async function createCanvasFromFile(
+ source: string,
+ canvas: HTMLCanvasElement,
+): Promise<string | undefined> {
+ const img = new Image(300, 300);
+ img.src = source;
+ canvas.width = img.width;
+ canvas.height = img.height;
+ return new Promise<string | undefined>((ok, bad) => {
+ img.addEventListener("load", () => {
+ try {
+ const code = drawIntoCanvasAndGetQR(img, canvas);
+ ok(code);
+ } catch (error) {
+ bad(error);
+ }
+ });
+ });
+}
+
+async function waitUntilReady(video: HTMLVideoElement): Promise<void> {
+ return new Promise((ok, _bad) => {
+ if (video.readyState === video.HAVE_ENOUGH_DATA) {
+ return ok();
+ }
+ setTimeout(waitUntilReady, 100);
+ });
+}
+
+export function QrReaderPage({ onDetected }: Props): VNode {
+ const videoRef = useRef<HTMLVideoElement>(null);
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+ const [error, setError] = useState<TranslatedString | undefined>();
+ const [value, setValue] = useState("");
+ const [show, setShow] = useState<"canvas" | "video" | "nothing">("nothing");
+
+ const { i18n } = useTranslationContext();
+
+ function 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 (!parseTalerUri(str)) {
+ setError(
+ i18n.str`URI is not valid. Taler URI should start with "taler://"`,
+ );
+ } else {
+ setError(undefined);
+ }
+ } else {
+ setError(undefined);
+ }
+ setValue(str);
+ }
+
+ async function startVideo() {
+ if (!videoRef.current || !canvasRef.current) {
+ return;
+ }
+ const video = videoRef.current;
+ if (!video || !video.played) return;
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: { facingMode: "environment" },
+ audio: false,
+ });
+ setShow("video");
+ setError(undefined);
+ video.srcObject = stream;
+ await video.play();
+ await waitUntilReady(video);
+ try {
+ const code = await createCanvasFromVideo(video, canvasRef.current);
+ if (code) {
+ onChangeDetect(code);
+ setShow("canvas");
+ }
+ stream.getTracks().forEach((e) => {
+ e.stop();
+ });
+ } catch (error) {
+ setError(i18n.str`something unexpected happen: ${error}`);
+ }
+ }
+
+ async function onFileRead(fileContent: string) {
+ if (!canvasRef.current) {
+ return;
+ }
+ setShow("nothing");
+ setError(undefined);
+ try {
+ const code = await createCanvasFromFile(fileContent, canvasRef.current);
+ if (code) {
+ onChangeDetect(code);
+ setShow("canvas");
+ } else {
+ setError(i18n.str`Could not found a QR code in the file`);
+ }
+ } catch (error) {
+ setError(i18n.str`something unexpected happen: ${error}`);
+ }
+ }
+ const uri = parseTalerUri(value);
+
+ return (
+ <Container>
+ <section>
+ <h1>
+ <i18n.Translate>
+ Scan a QR code or enter taler:// URI below
+ </i18n.Translate>
+ </h1>
+ <div style={{ justifyContent: "space-between", display: "flex" }}>
+ <div style={{ width: "75%" }}>
+ <TextField
+ label="Taler URI"
+ variant="filled"
+ fullWidth
+ value={value}
+ onChange={onChange}
+ />
+ </div>
+ {uri && (
+ <Button
+ disabled={!!error}
+ variant="contained"
+ color="success"
+ onClick={async () => {
+ if (uri) onDetected(uri);
+ }}
+ >
+ {(function (talerUri: TalerUri): VNode {
+ switch (talerUri.type) {
+ case TalerUriAction.Pay:
+ return <i18n.Translate>Pay invoice</i18n.Translate>;
+ case TalerUriAction.Withdraw:
+ return (
+ <i18n.Translate>Withdrawal from bank</i18n.Translate>
+ );
+ case TalerUriAction.Refund:
+ return <i18n.Translate>Claim refund</i18n.Translate>;
+ case TalerUriAction.PayPull:
+ return <i18n.Translate>Pay invoice</i18n.Translate>;
+ case TalerUriAction.PayPush:
+ return <i18n.Translate>Accept payment</i18n.Translate>;
+ case TalerUriAction.PayTemplate:
+ return <i18n.Translate>Complete order</i18n.Translate>;
+ case TalerUriAction.Restore:
+ return <i18n.Translate>Restore wallet</i18n.Translate>;
+ case TalerUriAction.DevExperiment:
+ return <i18n.Translate>Enable experiment</i18n.Translate>;
+ case TalerUriAction.WithdrawExchange:
+ return (
+ <i18n.Translate>Withdraw from exchange</i18n.Translate>
+ );
+ case TalerUriAction.AddExchange:
+ return <i18n.Translate>Add exchange</i18n.Translate>;
+ default: {
+ assertUnreachable(talerUri);
+ }
+ }
+ })(uri)}
+ </Button>
+ )}
+ </div>
+ <Grid container justifyContent="space-around" columns={2}>
+ <Grid item xs={2}>
+ <p>{error && <Alert severity="error">{error}</Alert>}</p>
+ </Grid>
+ <Grid item xs={2}>
+ <p>
+ <Button variant="contained" onClick={startVideo}>
+ Use Camera
+ </Button>
+ </p>
+ </Grid>
+ <EnabledBySettings name="advancedMode">
+ <Grid item xs={2}>
+ <InputFile onChange={onFileRead}>Read QR from file</InputFile>
+ </Grid>
+ </EnabledBySettings>
+ </Grid>
+ </section>
+ <div>
+ <video
+ ref={videoRef}
+ style={{ display: show === "video" ? "unset" : "none" }}
+ playsInline={true}
+ />
+ <canvas
+ id="este"
+ class={QrCanvas}
+ ref={canvasRef}
+ style={{ display: show === "canvas" ? "unset " : "none" }}
+ />
+ </div>
+ </Container>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
deleted file mode 100644
index ca524f4e2..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample } from '../test-utils';
-import { ReserveCreated as TestedComponent } from './ReserveCreated';
-
-export default {
- title: 'wallet/manual withdraw/reserve created',
- component: TestedComponent,
- argTypes: {
- }
-};
-
-
-export const InitialState = createExample(TestedComponent, {
- reservePub: 'ASLKDJQWLKEJASLKDJSADLKASJDLKSADJ',
- paytos: [
- 'payto://x-taler-bank/bank.taler:5882/exchangeminator?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG',
- 'payto://x-taler-bank/international-bank.com/myaccount?amount=COL%3A1&message=Taler+Withdrawal+TYQTE7VA4M9GZQ4TR06YBNGA05AJGMFNSK4Q62NXR2FKNDB1J4EX',
- ]
-});
-
diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
deleted file mode 100644
index e01336e02..000000000
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Fragment, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { QR } from "../components/QR";
-import { ButtonBox, FontIcon, WalletBox } from "../components/styled";
-
-export interface Props {
- reservePub: string;
- paytos: string[];
- onBack: () => void;
-}
-
-export function ReserveCreated({ reservePub, paytos, onBack }: Props): VNode {
- const [opened, setOpened] = useState(-1)
- return (
- <WalletBox>
- <section>
- <h2>Reserve created!</h2>
- <p>Now you need to send money to the exchange to one of the following accounts</p>
- <p>To complete the setup of the reserve, you must now initiate a wire transfer using the given wire transfer subject and crediting the specified amount to the indicated account of the exchange.</p>
- </section>
- <section>
- <ul>
- {paytos.map((href, idx) => {
- const url = new URL(href)
- return <li key={idx}><p>
- <a href="" onClick={(e) => { setOpened(o => o === idx ? -1 : idx); e.preventDefault() }}>{url.pathname}</a>
- {opened === idx && <Fragment>
- <p>If your system supports RFC 8905, you can do this by opening <a href={href}>this URI</a> or scan the QR with your wallet</p>
- <QR text={href} />
- </Fragment>}
- </p></li>
- })}
- </ul>
- </section>
- <footer>
- <ButtonBox onClick={onBack}><FontIcon>&#x2190;</FontIcon></ButtonBox>
- <div />
- </footer>
- </WalletBox>
- );
-}
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
index a04a0b4fd..cd43c4526 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,39 +15,77 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { SettingsView as TestedComponent } from './Settings';
+import * as tests from "@gnu-taler/web-util/testing";
+import { SettingsView as TestedComponent } from "./Settings.js";
+import { WalletCoreVersion } from "@gnu-taler/taler-util";
export default {
- title: 'wallet/settings',
+ title: "settings",
component: TestedComponent,
argTypes: {
setDeviceName: () => Promise.resolve(),
- }
+ },
+};
+
+const version = {
+ coreVersion: {
+ exchange: "12:0:0",
+ merchant: "2:0:1",
+ bank: "0:0:0",
+ hash: "d439c3e1bc743f2aa47de4457953dba6ecb0e20f",
+ version: "1:2:3",
+ devMode: false,
+ bankConversionApiRange: "0:0:0",
+ bankIntegrationApiRange: "0:0:0",
+ corebankApiRange: "0:0:0",
+ implementationGitHash: "d439c3e1bc743f2aa47de4457953dba6ecb0e20f",
+ implementationSemver: "0.9.0-dev.1",
+ } satisfies WalletCoreVersion,
+ webexVersion: {
+ version: "0.9.0.13",
+ hash: "d439c3e1bc743f2aa47de4457953dba6ecb0e20f",
+ },
};
-export const AllOff = createExample(TestedComponent, {
- deviceName: 'this-is-the-device-name',
+export const AllOff = tests.createExample(TestedComponent, {
+ deviceName: "this-is-the-device-name",
+ advanceToggle: { value: false, button: {} },
+ autoOpenToggle: { value: false, button: {} },
+ langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
+ ...version,
});
-export const OneChecked = createExample(TestedComponent, {
- deviceName: 'this-is-the-device-name',
- permissionsEnabled: true,
+export const OneChecked = tests.createExample(TestedComponent, {
+ deviceName: "this-is-the-device-name",
+ advanceToggle: { value: false, button: {} },
+ autoOpenToggle: { value: false, button: {} },
+ langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
+ ...version,
});
-export const WithOneExchange = createExample(TestedComponent, {
- deviceName: 'this-is-the-device-name',
- permissionsEnabled: true,
+export const WithOneExchange = tests.createExample(TestedComponent, {
+ deviceName: "this-is-the-device-name",
+ advanceToggle: { value: false, button: {} },
+ autoOpenToggle: { value: false, button: {} },
+ langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'http://exchange.taler',
- paytoUris: ['payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator']
- }]
+ ...version,
});
+
+export const WithExchangeInDifferentState = tests.createExample(
+ TestedComponent,
+ {
+ deviceName: "this-is-the-device-name",
+ advanceToggle: { value: false, button: {} },
+ autoOpenToggle: { value: false, button: {} },
+ langToggle: { value: false, button: {} },
+ setDeviceName: () => Promise.resolve(),
+ ...version,
+ },
+);
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
index 8d18586b1..0d0a31a2d 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
@@ -1,107 +1,290 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ LibtoolVersion,
+ TranslatedString,
+ WalletCoreVersion
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Checkbox } from "../components/Checkbox.js";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
+import { Part } from "../components/Part.js";
+import { SelectList } from "../components/SelectList.js";
+import {
+ Input,
+ SubTitle,
+ WarningBox
+} from "../components/styled/index.js";
+import { useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useBackupDeviceName } from "../hooks/useBackupDeviceName.js";
+import { useSettings } from "../hooks/useSettings.js";
+import { ToggleHandler } from "../mui/handlers.js";
+import { Settings } from "../platform/api.js";
+import { platform } from "../platform/foreground.js";
+import { WALLET_CORE_SUPPORTED_VERSION } from "../wxApi.js";
-import { ExchangeListItem, i18n } from "@gnu-taler/taler-util";
-import { VNode, h, Fragment } from "preact";
-import { Checkbox } from "../components/Checkbox";
-import { EditableText } from "../components/EditableText";
-import { SelectList } from "../components/SelectList";
-import { ButtonPrimary, ButtonSuccess, WalletBox } from "../components/styled";
-import { useDevContext } from "../context/devContext";
-import { useBackupDeviceName } from "../hooks/useBackupDeviceName";
-import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
-import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
-import { useLang } from "../hooks/useLang";
-import * as wxApi from "../wxApi";
+const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
export function SettingsPage(): VNode {
- const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
- const { devMode, toggleDevMode } = useDevContext()
- const { name, update } = useBackupDeviceName()
- const [lang, changeLang] = useLang()
- const exchangesHook = useAsyncAsHook(() => wxApi.listExchanges());
+ const [settings, updateSettings] = useSettings();
+ const { safely } = useAlertContext();
+ const { name, update } = useBackupDeviceName();
+ const webex = platform.getWalletWebExVersion();
+ const api = useBackendContext();
- return <SettingsView
- lang={lang} changeLang={changeLang}
- knownExchanges={!exchangesHook || exchangesHook.hasError ? [] : exchangesHook.response.exchanges}
- deviceName={name} setDeviceName={update}
- permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions}
- developerMode={devMode} toggleDeveloperMode={toggleDevMode}
- />;
+ const hook = useAsyncAsHook(async () => {
+ const version = await api.wallet.call(WalletApiOperation.GetVersion, {});
+ return { version };
+ });
+
+ const version = hook && !hook.hasError ? hook.response.version : undefined
+
+ return (
+ <SettingsView
+ deviceName={name}
+ setDeviceName={update}
+ autoOpenToggle={{
+ value: settings.autoOpen,
+ button: {
+ onClick: safely("update support injection", async () => {
+ updateSettings("autoOpen", !settings.autoOpen);
+ }),
+ },
+ }}
+ advanceToggle={{
+ value: settings.advancedMode,
+ button: {
+ onClick: safely("update advance mode", async () => {
+ updateSettings("advancedMode", !settings.advancedMode);
+ }),
+ },
+ }}
+ langToggle={{
+ value: settings.langSelector,
+ button: {
+ onClick: safely("update lang selector", async () => {
+ updateSettings("langSelector", !settings.langSelector);
+ }),
+ },
+ }}
+ webexVersion={{
+ version: webex.version,
+ hash: GIT_HASH,
+ }}
+ coreVersion={version}
+ />
+ );
}
export interface ViewProps {
- lang: string;
- changeLang: (s: string) => void;
deviceName: string;
setDeviceName: (s: string) => Promise<void>;
- permissionsEnabled: boolean;
- togglePermissions: () => void;
- developerMode: boolean;
- toggleDeveloperMode: () => void;
- knownExchanges: Array<ExchangeListItem>;
+ autoOpenToggle: ToggleHandler;
+ advanceToggle: ToggleHandler;
+ langToggle: ToggleHandler;
+ coreVersion: WalletCoreVersion | undefined;
+ webexVersion: {
+ version: string;
+ hash: string | undefined;
+ };
}
-import { strings as messages } from '../i18n/strings'
-
-type LangsNames = {
- [P in keyof typeof messages]: string
-}
-
-const names: LangsNames = {
- es: 'Español [es]',
- en: 'English [en]',
- fr: 'Français [fr]',
- de: 'Deutsch [de]',
- sv: 'Svenska [sv]',
- it: 'Italiano [it]',
-}
+export function SettingsView({
+ autoOpenToggle,
+ advanceToggle,
+ langToggle,
+ coreVersion,
+ webexVersion,
+}: ViewProps): VNode {
+ const { i18n, lang, supportedLang, changeLanguage } = useTranslationContext();
+ const api = useBackendContext();
-export function SettingsView({ knownExchanges, lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode {
return (
- <WalletBox>
+ <Fragment>
<section>
+ <SubTitle>
+ <i18n.Translate>Navigator</i18n.Translate>
+ </SubTitle>
+ <Checkbox
+ label={i18n.str`Automatically open wallet`}
+ name="autoOpen"
+ description={
+ <i18n.Translate>
+ Open the wallet when a payment action is found.
+ </i18n.Translate>
+ }
+ enabled={autoOpenToggle.value!}
+ onToggle={autoOpenToggle.button.onClick!}
+ />
- <h2><i18n.Translate>Known exchanges</i18n.Translate></h2>
- {!knownExchanges || !knownExchanges.length ? <div>
- No exchange yet!
- </div> :
- <table>
- {knownExchanges.map(e => <tr>
- <td>{e.currency}</td>
- <td><a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a></td>
- </tr>)}
- </table>
- }
-
- <h2><i18n.Translate>Permissions</i18n.Translate></h2>
- <Checkbox label="Automatically open wallet based on page content"
- name="perm"
- description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)"
- enabled={permissionsEnabled} onToggle={togglePermissions}
+ <SubTitle>
+ <i18n.Translate>Version Info</i18n.Translate>
+ </SubTitle>
+ <Part
+ title={i18n.str`Web Extension`}
+ text={
+ <span>
+ {webexVersion.version}{" "}
+ <EnabledBySettings name="advancedMode">
+ {webexVersion.hash}
+ </EnabledBySettings>
+ </span>
+ }
/>
- <h2>Config</h2>
- <Checkbox label="Developer mode"
+ {coreVersion && (
+ <Fragment>
+ {LibtoolVersion.compare(
+ coreVersion.version,
+ WALLET_CORE_SUPPORTED_VERSION,
+ )?.compatible ? undefined : (
+ <WarningBox>
+ <i18n.Translate>
+ The version of wallet core is not supported. (supported
+ version: {WALLET_CORE_SUPPORTED_VERSION}, wallet version: {coreVersion.version})
+ </i18n.Translate>
+ </WarningBox>
+ )}
+ <EnabledBySettings name="advancedMode">
+ <Part
+ title={i18n.str`Exchange compatibility`}
+ text={<span>{coreVersion.exchange}</span>}
+ />
+ <Part
+ title={i18n.str`Merchant compatibility`}
+ text={<span>{coreVersion.merchant}</span>}
+ />
+ <Part
+ title={i18n.str`Bank compatibility`}
+ text={<span>{coreVersion.bank}</span>}
+ />
+ <Part
+ title={i18n.str`Wallet Core compatibility`}
+ text={<span>{coreVersion.version}</span>}
+ />
+ </EnabledBySettings>
+ </Fragment>
+ )}
+ <SubTitle>
+ <i18n.Translate>Settings</i18n.Translate>
+ </SubTitle>
+ <Checkbox
+ label={i18n.str`Enable developer mode`}
name="devMode"
- description="(More options and information useful for debugging)"
- enabled={developerMode} onToggle={toggleDeveloperMode}
+ description={i18n.str`Show more information and options in the UI`}
+ enabled={advanceToggle.value!}
+ onToggle={advanceToggle.button.onClick!}
/>
+ <EnabledBySettings name="advancedMode">
+ <AdvanceSettings />
+ </EnabledBySettings>
+ <EnabledBySettings name="langSelector">
+ <SubTitle>
+ <i18n.Translate>Display</i18n.Translate>
+ </SubTitle>
+ <Input>
+ <SelectList
+ label={<i18n.Translate>Current Language</i18n.Translate>}
+ list={supportedLang}
+ name="lang"
+ value={lang}
+ onChange={(v) => changeLanguage(v)}
+ />
+ </Input>
+ </EnabledBySettings>
+ </section>
+ </Fragment>
+ );
+}
+
+type Info = { label: TranslatedString; description: TranslatedString };
+type Options = {
+ [k in keyof Settings]?: Info;
+};
+function AdvanceSettings(): VNode {
+ const [settings, updateSettings] = useSettings();
+ const api = useBackendContext();
+ const { i18n } = useTranslationContext();
+ const o: Options = {
+ backup: {
+ label: i18n.str`Show backup feature`,
+ description: i18n.str`Backup integration still in beta.`,
+ },
+ suspendIndividualTransaction: {
+ label: i18n.str`Show suspend/resume transaction`,
+ description: i18n.str`Prevent transaction from doing network request.`,
+ },
+ showRefeshTransactions: {
+ label: i18n.str`Show refresh transaction type in the transaction list`,
+ description: i18n.str`Refresh transaction will be hidden by default if the refresh operation doesn't have fee.`,
+ },
+ extendedAccountTypes: {
+ label: i18n.str`Show more account types on deposit`,
+ description: i18n.str`Extends the UI to more payment target types.`,
+ },
+ showJsonOnError: {
+ label: i18n.str`Show JSON on error`,
+ description: i18n.str`Print more information about the error. Useful for debugging.`,
+ },
+ walletAllowHttp: {
+ label: i18n.str`Allow HTTP connections`,
+ description: i18n.str`Using HTTP connection may be faster but unsafe (wallet restart required)`,
+ },
+ langSelector: {
+ label: i18n.str`Lang selector`,
+ description: i18n.str`Allows to manually change the language of the UI. Otherwise it will be automatically selected by your browser configuration.`,
+ },
+ showExchangeManagement: {
+ label: i18n.str`Edit exchange management`,
+ description: i18n.str`Allows to see the list of exchange, remove, add and switch before withdrawal.`,
+ },
+ selectTosFormat: {
+ label: i18n.str`Select terms of service format`,
+ description: i18n.str`Allows to render the terms of service on different format selected by the user.`,
+ },
+ showWalletActivity: {
+ label: i18n.str`Show wallet activity`,
+ description: i18n.str`Show the wallet notification and observability event in the UI.`,
+ },
+ };
+ return (
+ <Fragment>
+ <section>
+ {Object.entries(o).map(([name, { label, description }]) => {
+ const settingsName = name as keyof Settings;
+ return (
+ <Checkbox
+ label={label}
+ name={name}
+ key={name}
+ description={description}
+ enabled={settings[settingsName]}
+ onToggle={async () => {
+ updateSettings(settingsName, !settings[settingsName]);
+ await api.background.call("reinitWallet", undefined);
+ }}
+ />
+ );
+ })}
</section>
- </WalletBox>
- )
+ </Fragment>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index 535509cef..194f0e0bb 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,262 +15,612 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import {
+ AbsoluteTime,
+ AmountString,
PaymentStatus,
- TransactionCommon, TransactionDeposit, TransactionPayment,
- TransactionRefresh, TransactionRefund, TransactionTip, TransactionType,
+ RefreshReason,
+ TalerPreciseTimestamp,
+ TalerProtocolTimestamp,
+ TransactionCommon,
+ TransactionDeposit,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionPayment,
+ TransactionPeerPullCredit,
+ TransactionPeerPullDebit,
+ TransactionPeerPushCredit,
+ TransactionPeerPushDebit,
+ TransactionRefresh,
+ TransactionRefund,
+ TransactionType,
TransactionWithdrawal,
+ WithdrawalDetails,
WithdrawalType
-} from '@gnu-taler/taler-util';
-import { createExample } from '../test-utils';
-import { TransactionView as TestedComponent } from './Transaction';
+} from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import beer from "../../static-dev/beer.png";
+import { TransactionView as TestedComponent } from "./Transaction.js";
export default {
- title: 'wallet/history/details',
+ title: "transaction details",
component: TestedComponent,
argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
+ onRetry: { action: "onRetry" },
+ onDelete: { action: "onDelete" },
+ onCancel: { action: "onCancel" },
+ onBack: { action: "onBack" },
+ },
};
-const commonTransaction = {
- amountRaw: 'KUDOS:11',
- amountEffective: 'KUDOS:9.2',
- pending: false,
- timestamp: {
- t_ms: new Date().getTime()
+const commonTransaction: TransactionCommon = {
+ error: undefined,
+ amountRaw: "KUDOS:11" as AmountString,
+ amountEffective: "KUDOS:9.2" as AmountString,
+ txState: {
+ major: TransactionMajorState.Done,
},
- transactionId: '12',
-} as TransactionCommon
+ txActions: [],
+ timestamp: TalerProtocolTimestamp.now(),
+ transactionId: "txn:deposit:12" as TransactionIdStr,
+ type: TransactionType.Deposit,
+} as Omit<
+ Omit<Omit<TransactionCommon, "extendedStatus">, "frozen">,
+ "pending"
+> as TransactionCommon;
+
+import merchantIcon from "../../static-dev/merchant-icon.jpeg";
const exampleData = {
withdraw: {
...commonTransaction,
type: TransactionType.Withdrawal,
- exchangeBaseUrl: 'http://exchange.taler',
+ exchangeBaseUrl: "http://exchange.taler",
withdrawalDetails: {
+ reserveIsReady: false,
confirmed: false,
- exchangePaytoUris: ['payto://x-taler-bank/bank/account'],
+ reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+ exchangePaytoUris: ["payto://x-taler-bank/bank.demo.taler.net/Exchange"],
type: WithdrawalType.ManualTransfer,
- }
+ },
} as TransactionWithdrawal,
payment: {
...commonTransaction,
- amountEffective: 'KUDOS:11',
+ amountEffective: "KUDOS:12" as AmountString,
type: TransactionType.Payment,
+ posConfirmation: undefined,
info: {
- contractTermsHash: 'ASDZXCASD',
+ contractTermsHash: "ASDZXCASD",
merchant: {
- name: 'the merchant',
+ name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
},
- orderId: '2021.167-03NPY6MCYMVGT',
+ orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
- fulfillmentMessage: '',
+ fulfillmentMessage: "",
+ // delivery_date: { t_s: 1 },
+ // delivery_location: {
+ // address_lines: [""],
+ // },
},
- proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
+ refunds: [],
+ refundPending: undefined,
+ totalRefundEffective: "KUDOS:0" as AmountString,
+ totalRefundRaw: "KUDOS:0" as AmountString,
+ proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
+ refundQueryActive: false,
} as TransactionPayment,
deposit: {
...commonTransaction,
type: TransactionType.Deposit,
- depositGroupId: '#groupId',
- targetPaytoUri: 'payto://x-taler-bank/bank/account',
+ wireTransferDeadline: {
+ t_s: new Date().getTime() / 1000,
+ },
+ depositGroupId: "#groupId",
+ targetPaytoUri: "payto://x-taler-bank/bank.demo.taler.net/Exchange",
} as TransactionDeposit,
refresh: {
...commonTransaction,
type: TransactionType.Refresh,
- exchangeBaseUrl: 'http://exchange.taler',
+ refreshInputAmount: "KUDOS:1" as AmountString,
+ refreshOutputAmount: "KUDOS:0.5" as AmountString,
+ exchangeBaseUrl: "http://exchange.taler",
+ refreshReason: RefreshReason.Manual,
} as TransactionRefresh,
- tip: {
- ...commonTransaction,
- type: TransactionType.Tip,
- merchantBaseUrl: 'http://merchant.taler',
- } as TransactionTip,
refund: {
...commonTransaction,
type: TransactionType.Refund,
- refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
- info: {
- contractTermsHash: 'ASDZXCASD',
+ refundedTransactionId:
+ "payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
+ paymentInfo: {
merchant: {
- name: 'the merchant',
+ name: "The merchant",
},
- orderId: '2021.167-03NPY6MCYMVGT',
- products: [],
- summary: 'the summary',
- fulfillmentMessage: '',
+ summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
+ summary_i18n: {},
},
+ refundPending: undefined,
} as TransactionRefund,
-}
+ push_credit: {
+ ...commonTransaction,
+ type: TransactionType.PeerPushCredit,
+ info: {
+ expiration: {
+ t_s: new Date().getTime() / 1000 + 2 * 60 * 60,
+ },
+ summary: "take this money",
+ completed: true,
+ },
+ kycUrl: undefined,
+ exchangeBaseUrl: "https://exchange.taler.net",
+ } as TransactionPeerPushCredit,
+ push_debit: {
+ ...commonTransaction,
+ type: TransactionType.PeerPushDebit,
+ talerUri:
+ "taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
+ info: {
+ expiration: {
+ t_s: new Date().getTime() / 1000 + 2 * 60 * 60,
+ },
+ summary: "take this money",
+ completed: true,
+ },
+ exchangeBaseUrl: "https://exchange.taler.net",
+ } as TransactionPeerPushDebit,
+ pull_credit: {
+ ...commonTransaction,
+ type: TransactionType.PeerPullCredit,
+ talerUri:
+ "taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0",
+ info: {
+ expiration: {
+ t_s: new Date().getTime() / 1000 + 2 * 60 * 60,
+ },
+ summary: "pay me, please?",
+ completed: true,
+ },
+ kycUrl: undefined,
+ exchangeBaseUrl: "https://exchange.taler.net",
+ } as TransactionPeerPullCredit,
+ pull_debit: {
+ ...commonTransaction,
+ type: TransactionType.PeerPullDebit,
+ info: {
+ expiration: {
+ t_s: new Date().getTime() / 1000 + 2 * 60 * 60,
+ },
+ summary: "pay me, please?",
+ completed: true,
+ },
+ exchangeBaseUrl: "https://exchange.taler.net",
+ } as TransactionPeerPullDebit,
+};
const transactionError = {
- code: 2000,
- details: "details",
- hint: "this is a hint for the error",
- message: 'message'
-}
+ code: 7005,
+ details: {
+ requestUrl:
+ "http://merchant-backend.taler:9966/orders/2021.340-02AD5XCC97MQM/pay",
+ httpStatusCode: 410,
+ errorResponse: {
+ code: 2161,
+ hint: "The payment is too late, the offer has expired.",
+ },
+ },
+ when: AbsoluteTime.now(),
+ hint: "Error: WALLET_UNEXPECTED_REQUEST_ERROR",
+ message: "Unexpected error code in response",
+};
-export const Withdraw = createExample(TestedComponent, {
- transaction: exampleData.withdraw
+export const Withdraw = tests.createExample(TestedComponent, {
+ transaction: exampleData.withdraw,
});
-export const WithdrawError = createExample(TestedComponent, {
+export const WithdrawFiveMinutesAgo = tests.createExample(
+ TestedComponent,
+ () => ({
+ transaction: {
+ ...exampleData.withdraw,
+ timestamp: TalerPreciseTimestamp.fromSeconds(
+ new Date().getTime() / 1000 - 60 * 5,
+ ),
+ },
+ }),
+);
+
+export const WithdrawFiveMinutesAgoAndPending = tests.createExample(
+ TestedComponent,
+ () => ({
+ transaction: {
+ ...exampleData.withdraw,
+ timestamp: TalerPreciseTimestamp.fromSeconds(
+ new Date().getTime() / 1000 - 60 * 5,
+ ),
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+ }),
+);
+
+export const WithdrawError = tests.createExample(TestedComponent, {
transaction: {
...exampleData.withdraw,
error: transactionError,
},
});
-export const WithdrawPending = createExample(TestedComponent, {
- transaction: { ...exampleData.withdraw, pending: true },
+export const WithdrawErrorKYC = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.withdraw,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ kycUrl:
+ "http://localhost:6666/oauth/v2/login?client_id=taler-exchange&redirect_uri=http%3A%2F%2Flocalhost%3A8081%2F%2Fkyc-proof%2F59WFS5VXXY3CEE25BM45XPB7ZCDQZNZ46PJCMNXK05P65T9M1X90%2FKYC-PROVIDER-MYPROV%2F1",
+ },
});
+export const WithdrawPendingManual = tests.createExample(
+ TestedComponent,
+ () => ({
+ transaction: {
+ ...exampleData.withdraw,
+ withdrawalDetails: {
+ type: WithdrawalType.ManualTransfer,
+ exchangePaytoUris: ["payto://iban/ES8877998399652238"],
+ reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+ exchangeCreditAccountDetails: [{
+ paytoUri: "payto://IBAN/1231231231",
+ },
+ {
+ paytoUri: "payto://IBAN/2342342342",
+ }],
+ } as WithdrawalDetails,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+ }),
+);
+
+export const WithdrawPendingTalerBankUnconfirmed = tests.createExample(
+ TestedComponent,
+ {
+ transaction: {
+ ...exampleData.withdraw,
+ withdrawalDetails: {
+ type: WithdrawalType.TalerBankIntegrationApi,
+ confirmed: false,
+ reserveIsReady: false,
+ reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+ bankConfirmationUrl: "http://bank.demo.taler.net",
+ },
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+ },
+);
+
+export const WithdrawPendingTalerBankConfirmed = tests.createExample(
+ TestedComponent,
+ {
+ transaction: {
+ ...exampleData.withdraw,
+ withdrawalDetails: {
+ type: WithdrawalType.TalerBankIntegrationApi,
+ confirmed: true,
+ reserveIsReady: false,
+ reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+ },
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+ },
+);
-export const Payment = createExample(TestedComponent, {
- transaction: exampleData.payment
+export const Payment = tests.createExample(TestedComponent, {
+ transaction: exampleData.payment,
});
-export const PaymentError = createExample(TestedComponent, {
+export const PaymentWithPosConfirmation = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- error: transactionError
+ posConfirmation: "123123\n3345345\n567567",
},
});
-export const PaymentWithoutFee = createExample(TestedComponent, {
+export const PaymentError = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- amountRaw: 'KUDOS:11',
+ error: transactionError,
+ },
+});
- }
+export const PaymentWithRefund = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:12" as AmountString,
+ totalRefundEffective: "KUDOS:1" as AmountString,
+ totalRefundRaw: "KUDOS:1" as AmountString,
+ refunds: [
+ {
+ transactionId: "1123123",
+ amountRaw: "KUDOS:1" as AmountString,
+ amountEffective: "KUDOS:1" as AmountString,
+ timestamp: TalerProtocolTimestamp.fromSeconds(1546546544),
+ },
+ ],
+ },
});
-export const PaymentPending = createExample(TestedComponent, {
- transaction: { ...exampleData.payment, pending: true },
+export const PaymentWithDeliveryDate = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:12" as AmountString,
+ info: {
+ ...exampleData.payment.info,
+ // delivery_date: {
+ // t_s: new Date().getTime() / 1000,
+ // },
+ },
+ },
});
-export const PaymentWithProducts = createExample(TestedComponent, {
+export const PaymentWithDeliveryAddr = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
+ amountRaw: "KUDOS:12" as AmountString,
info: {
...exampleData.payment.info,
- summary: 'this order has 5 products',
- products: [{
- description: 't-shirt',
- unit: 'shirts',
- quantity: 1,
- }, {
- description: 't-shirt',
- unit: 'shirts',
- quantity: 1,
- }, {
- description: 'e-book',
- }, {
- description: 'beer',
- unit: 'pint',
- quantity: 15,
- }, {
- description: 'beer',
- unit: 'pint',
- quantity: 15,
- }]
- }
- } as TransactionPayment,
+ // delivery_location: {
+ // country: "Argentina",
+ // street: "Elm Street",
+ // district: "CABA",
+ // post_code: "1101",
+ // },
+ },
+ },
});
-export const PaymentWithLongSummary = createExample(TestedComponent, {
+export const PaymentWithDeliveryFull = tests.createExample(TestedComponent, {
transaction: {
...exampleData.payment,
+ amountRaw: "KUDOS:12" as AmountString,
info: {
...exampleData.payment.info,
- summary: 'this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, ',
- products: [{
- description: 'an xl sized t-shirt with some drawings on it, color pink',
- unit: 'shirts',
- quantity: 1,
- }, {
- description: 'beer',
- unit: 'pint',
- quantity: 15,
- }]
- }
+ // delivery_date: {
+ // t_s: new Date().getTime() / 1000,
+ // },
+ // delivery_location: {
+ // country: "Argentina",
+ // street: "Elm Street",
+ // district: "CABA",
+ // post_code: "1101",
+ // },
+ },
+ },
+});
+
+export const PaymentWithRefundPending = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:12" as AmountString,
+ refundPending: "KUDOS:3" as AmountString,
+ totalRefundEffective: "KUDOS:1" as AmountString,
+ totalRefundRaw: "KUDOS:1" as AmountString,
+ },
+});
+
+export const PaymentWithFeeAndRefund = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:11" as AmountString,
+ totalRefundEffective: "KUDOS:1" as AmountString,
+ totalRefundRaw: "KUDOS:1" as AmountString,
+ },
+});
+
+export const PaymentWithFeeAndRefundFee = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:11" as AmountString,
+ totalRefundEffective: "KUDOS:1" as AmountString,
+ totalRefundRaw: "KUDOS:2" as AmountString,
+ },
+});
+
+export const PaymentWithoutFee = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:12" as AmountString,
+ },
+});
+
+export const PaymentPending = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
+});
+
+export const PaymentWithProducts = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary: "summary of 5 products",
+ products: [
+ {
+ description: "t-shirt",
+ unit: "shirts",
+ quantity: 1,
+ },
+ {
+ description: "t-shirt",
+ unit: "shirts",
+ quantity: 1,
+ },
+ {
+ description: "e-book",
+ },
+ {
+ description: "beer",
+ unit: "pint",
+ quantity: 15,
+ image: beer,
+ },
+ {
+ description: "beer",
+ unit: "pint",
+ quantity: 15,
+ image: beer,
+ },
+ ],
+ },
} as TransactionPayment,
});
+export const PaymentWithLongSummary = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ info: {
+ ...exampleData.payment.info,
+ summary:
+ "this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, ",
+ products: [
+ {
+ description:
+ "an xl sized t-shirt with some drawings on it, color pink",
+ unit: "shirts",
+ quantity: 1,
+ },
+ {
+ description: "beer",
+ unit: "pint",
+ quantity: 15,
+ },
+ ],
+ },
+ } as TransactionPayment,
+});
-export const Deposit = createExample(TestedComponent, {
- transaction: exampleData.deposit
+export const Deposit = tests.createExample(TestedComponent, {
+ transaction: exampleData.deposit,
+});
+export const DepositTalerBank = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.deposit,
+ targetPaytoUri: "payto://x-taler-bank/bank.demo.taler.net/Exchange",
+ },
+});
+export const DepositBitcoin = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.deposit,
+ amountRaw: "BITCOINBTC:0.0000011" as AmountString,
+ amountEffective: "BITCOINBTC:0.00000092" as AmountString,
+ targetPaytoUri:
+ "payto://bitcoin/bcrt1q6ps8qs6v8tkqrnru4xqqqa6rfwcx5ufpdfqht4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
+ },
+});
+export const DepositIBAN = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.deposit,
+ targetPaytoUri: "payto://iban/ES8877998399652238",
+ },
});
-export const DepositError = createExample(TestedComponent, {
+export const DepositError = tests.createExample(TestedComponent, {
transaction: {
...exampleData.deposit,
- error: transactionError
+ error: transactionError,
},
});
-export const DepositPending = createExample(TestedComponent, {
- transaction: { ...exampleData.deposit, pending: true }
+export const DepositPending = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.deposit,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
});
-export const Refresh = createExample(TestedComponent, {
- transaction: exampleData.refresh
+export const Refresh = tests.createExample(TestedComponent, {
+ transaction: exampleData.refresh,
});
-export const RefreshError = createExample(TestedComponent, {
+export const RefreshError = tests.createExample(TestedComponent, {
transaction: {
...exampleData.refresh,
- error: transactionError
+ error: transactionError,
},
});
-export const Tip = createExample(TestedComponent, {
- transaction: exampleData.tip
+export const Refund = tests.createExample(TestedComponent, {
+ transaction: exampleData.refund,
});
-export const TipError = createExample(TestedComponent, {
+export const RefundError = tests.createExample(TestedComponent, {
transaction: {
- ...exampleData.tip,
- error: transactionError
+ ...exampleData.refund,
+ error: transactionError,
},
});
-export const TipPending = createExample(TestedComponent, {
- transaction: { ...exampleData.tip, pending: true }
+export const RefundPending = tests.createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.refund,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
});
-export const Refund = createExample(TestedComponent, {
- transaction: exampleData.refund
+export const InvoiceCreditComplete = tests.createExample(TestedComponent, {
+ transaction: { ...exampleData.pull_credit },
});
-export const RefundError = createExample(TestedComponent, {
+export const InvoiceCreditIncomplete = tests.createExample(TestedComponent, {
transaction: {
- ...exampleData.refund,
- error: transactionError
+ ...exampleData.pull_credit,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
},
});
-export const RefundPending = createExample(TestedComponent, {
- transaction: { ...exampleData.refund, pending: true }
+export const InvoiceDebit = tests.createExample(TestedComponent, {
+ transaction: { ...exampleData.pull_debit },
});
-export const RefundWithProducts = createExample(TestedComponent, {
+export const TransferCredit = tests.createExample(TestedComponent, {
+ transaction: { ...exampleData.push_credit },
+});
+
+export const TransferDebitComplete = tests.createExample(TestedComponent, {
+ transaction: { ...exampleData.push_debit },
+});
+export const TransferDebitIncomplete = tests.createExample(TestedComponent, {
transaction: {
- ...exampleData.refund,
- info: {
- ...exampleData.refund.info,
- products: [{
- description: 't-shirt',
- }, {
- description: 'beer',
- }]
- }
- } as TransactionRefund,
+ ...exampleData.push_debit,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ },
});
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 8a97ad50c..1f0293352 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -1,233 +1,2030 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, AmountLike, Amounts, i18n, Transaction, TransactionType } from "@gnu-taler/taler-util";
-import { format } from "date-fns";
-import { Fragment, JSX, VNode, h } from "preact";
-import { route } from 'preact-router';
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ AmountString,
+ DenomLossEventType,
+ MerchantInfo,
+ NotificationType,
+ OrderShortInfo,
+ parsePaytoUri,
+ PaytoUri,
+ stringifyPaytoUri,
+ TalerErrorCode,
+ TalerPreciseTimestamp,
+ Transaction,
+ TransactionAction,
+ TransactionDeposit,
+ TransactionIdStr,
+ TransactionInternalWithdrawal,
+ TransactionMajorState,
+ TransactionMinorState,
+ TransactionType,
+ TransactionWithdrawal,
+ TranslatedString,
+ WithdrawalType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { styled } from "@linaria/react";
+import { isPast } from "date-fns";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
-import * as wxApi from "../wxApi";
-import { Pages } from "../NavigationBar";
-import emptyImg from "../../static/img/empty.png"
-import { Button, ButtonBox, ButtonBoxDestructive, ButtonDestructive, ButtonPrimary, ExtraLargeText, FontIcon, LargeText, ListOfProducts, PopupBox, Row, RowBorderGray, SmallLightText, WalletBox, WarningBox } from "../components/styled";
-import { ErrorMessage } from "../components/ErrorMessage";
-import { Part } from "../components/Part";
-
-export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
- const [transaction, setTransaction] = useState<
- Transaction | undefined
- >(undefined);
-
- useEffect(() => {
- const fetchData = async (): Promise<void> => {
- const res = await wxApi.getTransactions();
- const ts = res.transactions.filter(t => t.transactionId === tid);
- if (ts.length === 1) {
- setTransaction(ts[0]);
- } else {
- route(Pages.history);
- }
- };
- fetchData();
- }, []);
+import { Amount } from "../components/Amount.js";
+import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType.js";
+import { AlertView, ErrorAlertView } from "../components/CurrentAlerts.js";
+import { EnabledBySettings } from "../components/EnabledBySettings.js";
+import { Loading } from "../components/Loading.js";
+import { Kind, Part, PartPayto } from "../components/Part.js";
+import { QR } from "../components/QR.js";
+import { ShowFullContractTermPopup } from "../components/ShowFullContractTermPopup.js";
+import {
+ CenteredDialog,
+ ErrorBox,
+ InfoBox,
+ Link,
+ Overlay,
+ SmallLightText,
+ SubTitle,
+ SvgIcon,
+ WarningBox,
+} from "../components/styled/index.js";
+import { Time } from "../components/Time.js";
+import { alertFromError, useAlertContext } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
+import { Button } from "../mui/Button.js";
+import { SafeHandler } from "../mui/handlers.js";
+import { Pages } from "../NavigationBar.js";
+import refreshIcon from "../svg/refresh_24px.inline.svg";
+import { assertUnreachable } from "../utils/index.js";
+
+interface Props {
+ tid: string;
+ goToWalletHistory: (currency?: string) => Promise<void>;
+}
+
+export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
+ const transactionId = tid as TransactionIdStr; //FIXME: validate
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+ const state = useAsyncAsHook(
+ () =>
+ api.wallet.call(WalletApiOperation.GetTransactionById, {
+ transactionId,
+ }),
+ [transactionId],
+ );
+
+ useEffect(() =>
+ api.listener.onUpdateNotification(
+ [NotificationType.TransactionStateTransition],
+ state?.retry,
+ ),
+ );
- if (!transaction) {
- return <div><i18n.Translate>Loading ...</i18n.Translate></div>;
+ if (!state) {
+ return <Loading />;
}
- return <TransactionView
- transaction={transaction}
- onDelete={() => wxApi.deleteTransaction(tid).then(_ => history.go(-1))}
- onRetry={() => wxApi.retryTransaction(tid).then(_ => history.go(-1))}
- onBack={() => { route(Pages.history) }} />;
+
+ if (state.hasError) {
+ return (
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not load transaction information`,
+ state,
+ )}
+ />
+ );
+ }
+
+ const currency = Amounts.parse(state.response.amountRaw)?.currency;
+
+ return (
+ <TransactionView
+ transaction={state.response}
+ onCancel={async () => {
+ await api.wallet.call(WalletApiOperation.FailTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onSuspend={async () => {
+ await api.wallet.call(WalletApiOperation.SuspendTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onResume={async () => {
+ await api.wallet.call(WalletApiOperation.ResumeTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onAbort={async () => {
+ await api.wallet.call(WalletApiOperation.AbortTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onRetry={async () => {
+ await api.wallet.call(WalletApiOperation.RetryTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onDelete={async () => {
+ await api.wallet.call(WalletApiOperation.DeleteTransaction, {
+ transactionId,
+ });
+ goToWalletHistory(currency);
+ }}
+ onRefund={async (transactionId) => {
+ await api.wallet.call(WalletApiOperation.StartRefundQuery, {
+ transactionId,
+ });
+ }}
+ onBack={() => goToWalletHistory(currency)}
+ />
+ );
}
export interface WalletTransactionProps {
transaction: Transaction;
- onDelete: () => void;
- onRetry: () => void;
- onBack: () => void;
+ onCancel: () => Promise<void>;
+ onSuspend: () => Promise<void>;
+ onResume: () => Promise<void>;
+ onAbort: () => Promise<void>;
+ onDelete: () => Promise<void>;
+ onRetry: () => Promise<void>;
+ onRefund: (id: TransactionIdStr) => Promise<void>;
+ onBack: () => Promise<void>;
}
-export function TransactionView({ transaction, onDelete, onRetry, onBack }: WalletTransactionProps) {
+const PurchaseDetailsTable = styled.table`
+ width: 100%;
- function TransactionTemplate({ children }: { children: VNode[] }) {
- return <WalletBox>
- <section style={{ padding: 8, textAlign: 'center'}}>
- <ErrorMessage title={transaction?.error?.hint} />
- {transaction.pending && <WarningBox>This transaction is not completed</WarningBox>}
- </section>
- <section>
- <div style={{ textAlign: 'center' }}>
- {children}
- </div>
+ & > tr > td:nth-child(2n) {
+ text-align: right;
+ }
+`;
+
+type TransactionTemplateProps = Omit<
+ Omit<WalletTransactionProps, "onRefund">,
+ "onBack"
+> & {
+ children: ComponentChildren;
+};
+
+function TransactionTemplate({
+ transaction,
+ onDelete,
+ onRetry,
+ onAbort,
+ onResume,
+ onSuspend,
+ onCancel,
+ children,
+}: TransactionTemplateProps): VNode {
+ const { i18n } = useTranslationContext();
+ const [confirmBeforeForget, setConfirmBeforeForget] = useState(false);
+ const [confirmBeforeCancel, setConfirmBeforeCancel] = useState(false);
+ const { safely } = useAlertContext();
+ const [settings] = useSettings();
+
+ async function doCheckBeforeForget(): Promise<void> {
+ if (
+ transaction.txState.major === TransactionMajorState.Pending &&
+ transaction.type === TransactionType.Withdrawal
+ ) {
+ setConfirmBeforeForget(true);
+ } else {
+ onDelete();
+ }
+ }
+
+ async function doCheckBeforeCancel(): Promise<void> {
+ setConfirmBeforeCancel(true);
+ }
+
+ const showButton = getShowButtonStates(transaction);
+
+ return (
+ <Fragment>
+ <section style={{ padding: 8, textAlign: "center" }}>
+ {transaction?.error &&
+ // FIXME: wallet core should stop sending this error on KYC
+ transaction.error.code !==
+ TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED ? (
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`There was an error trying to complete the transaction.`,
+ transaction.error,
+ )}
+ />
+ ) : undefined}
+ {transaction.txState.major === TransactionMajorState.Pending &&
+ (transaction.txState.minor === TransactionMinorState.KycRequired ? (
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`KYC check required for the transaction to complete.`,
+ description:
+ transaction.kycUrl &&
+ typeof transaction.kycUrl === "string" ? (
+ <div>
+ <i18n.Translate>
+ Follow this link to the{` `}
+ <a
+ rel="noreferrer"
+ target="_bank"
+ href={transaction.kycUrl}
+ >
+ KYC verifier.
+ </a>
+ </i18n.Translate>
+ </div>
+ ) : (
+ i18n.str`No additional information has been provided.`
+ ),
+ }}
+ />
+ ) : transaction.txState.minor ===
+ TransactionMinorState.AmlRequired ? (
+ <WarningBox>
+ <i18n.Translate>
+ 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>
+ </InfoBox>
+ )}
+ {transaction.txState.major === TransactionMajorState.Failed && (
+ <ErrorBox>
+ <i18n.Translate>This transaction failed.</i18n.Translate>
+ </ErrorBox>
+ )}
+ {confirmBeforeForget ? (
+ <Overlay>
+ <CenteredDialog>
+ <header>
+ <i18n.Translate>Caution!</i18n.Translate>
+ </header>
+ <section>
+ <i18n.Translate>
+ If you have already wired money to the exchange you will loose
+ the chance to get the coins form it.
+ </i18n.Translate>
+ </section>
+ <footer>
+ <Button
+ variant="contained"
+ color="secondary"
+ onClick={
+ (async () =>
+ setConfirmBeforeForget(false)) as SafeHandler<void>
+ }
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+
+ <Button
+ variant="contained"
+ color="error"
+ onClick={safely("delete transaction", onDelete)}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </Button>
+ </footer>
+ </CenteredDialog>
+ </Overlay>
+ ) : undefined}
+ {confirmBeforeCancel ? (
+ <Overlay>
+ <CenteredDialog>
+ <header>
+ <i18n.Translate>Caution!</i18n.Translate>
+ </header>
+ <section>
+ <i18n.Translate>
+ Doing a cancellation while the transaction still active might
+ result in lost coins. Do you still want to cancel the
+ transaction?
+ </i18n.Translate>
+ </section>
+ <footer>
+ <Button
+ variant="contained"
+ color="secondary"
+ onClick={
+ (async () =>
+ setConfirmBeforeCancel(false)) as SafeHandler<void>
+ }
+ >
+ <i18n.Translate>No</i18n.Translate>
+ </Button>
+
+ <Button
+ variant="contained"
+ color="error"
+ onClick={safely("cancel active transaction", onCancel)}
+ >
+ <i18n.Translate>Yes</i18n.Translate>
+ </Button>
+ </footer>
+ </CenteredDialog>
+ </Overlay>
+ ) : undefined}
</section>
+ <section>{children}</section>
<footer>
- <ButtonBox onClick={onBack}><i18n.Translate> <FontIcon>&#x2190;</FontIcon> </i18n.Translate></ButtonBox>
+ <div />
<div>
- {transaction?.error ? <ButtonPrimary onClick={onRetry}><i18n.Translate>retry</i18n.Translate></ButtonPrimary> : null}
- <ButtonBoxDestructive onClick={onDelete}><i18n.Translate>&#x1F5D1;</i18n.Translate></ButtonBoxDestructive>
+ {showButton.abort && (
+ <Button
+ variant="contained"
+ onClick={safely("abort transaction", onAbort)}
+ >
+ <i18n.Translate>Abort</i18n.Translate>
+ </Button>
+ )}
+ {showButton.resume && settings.suspendIndividualTransaction && (
+ <Button
+ variant="contained"
+ onClick={safely("resume transaction", onResume)}
+ >
+ <i18n.Translate>Resume</i18n.Translate>
+ </Button>
+ )}
+ {showButton.suspend && settings.suspendIndividualTransaction && (
+ <Button
+ variant="contained"
+ onClick={safely("suspend transaction", onSuspend)}
+ >
+ <i18n.Translate>Suspend</i18n.Translate>
+ </Button>
+ )}
+ {showButton.fail && (
+ <Button
+ variant="contained"
+ color="error"
+ onClick={doCheckBeforeCancel as SafeHandler<void>}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </Button>
+ )}
+ {showButton.remove && (
+ <Button
+ variant="contained"
+ color="error"
+ onClick={doCheckBeforeForget as SafeHandler<void>}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </Button>
+ )}
</div>
</footer>
- </WalletBox>
- }
+ </Fragment>
+ );
+}
- function amountToString(text: AmountLike) {
- const aj = Amounts.jsonifyAmount(text)
- const amount = Amounts.stringifyValue(aj)
- return `${amount} ${aj.currency}`
- }
+export function TransactionView({
+ transaction,
+ onDelete,
+ onAbort,
+ // onBack,
+ onResume,
+ onSuspend,
+ onRetry,
+ onRefund,
+ onCancel,
+}: WalletTransactionProps): VNode {
+ const { i18n } = useTranslationContext();
+ const { safely } = useAlertContext();
+ const raw = Amounts.parseOrThrow(transaction.amountRaw);
+ const effective = Amounts.parseOrThrow(transaction.amountEffective);
- if (transaction.type === TransactionType.Withdrawal) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountRaw),
- Amounts.parseOrThrow(transaction.amountEffective),
- ).amount
- return <TransactionTemplate>
- <h2>Withdrawal</h2>
- <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
- <br />
- <Part title="Total withdrawn" text={amountToString(transaction.amountEffective)} kind='positive' />
- <Part title="Chosen amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
- <Part title="Exchange fee" text={amountToString(fee)} kind='negative' />
- <Part title="Exchange" text={new URL(transaction.exchangeBaseUrl).hostname} kind='neutral' />
- </TransactionTemplate>
- }
-
- const showLargePic = () => {
+ if (
+ transaction.type === TransactionType.Withdrawal ||
+ transaction.type === TransactionType.InternalWithdrawal
+ ) {
+ // const conversion =
+ // transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ // ? transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ // : [];
+ const blockedByKycOrAml =
+ transaction.txState.minor === TransactionMinorState.KycRequired ||
+ transaction.txState.minor === TransactionMinorState.AmlRequired;
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Withdrawal`}
+ total={effective}
+ kind="positive"
+ >
+ {transaction.exchangeBaseUrl}
+ </Header>
+ {transaction.txState.major !== TransactionMajorState.Pending ||
+ blockedByKycOrAml ? undefined : transaction.withdrawalDetails.type ===
+ WithdrawalType.ManualTransfer &&
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ? (
+ <Fragment>
+ <InfoBox>
+ {transaction.withdrawalDetails.exchangeCreditAccountDetails
+ .length > 1 ? (
+ <span>
+ <i18n.Translate>
+ Now the payment service provider is waiting for{" "}
+ <Amount value={raw} /> to be transferred. Select one of the
+ accounts and use the information below to complete the
+ operation by making a wire transfer from your bank account.
+ </i18n.Translate>
+ </span>
+ ) : (
+ <span>
+ <i18n.Translate>
+ Now the payment service provider is waiting for{" "}
+ <Amount value={raw} /> to be transferred. Use the
+ information below to complete the operation by making a wire
+ transfer from your bank account.
+ </i18n.Translate>
+ </span>
+ )}
+ </InfoBox>
+ <BankDetailsByPaytoType
+ amount={raw}
+ accounts={
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ }
+ subject={transaction.withdrawalDetails.reservePub}
+ />
+ </Fragment>
+ ) : (
+ //integrated bank withdrawal
+ <ShowWithdrawalDetailForBankIntegrated transaction={transaction} />
+ )}
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <WithdrawDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
+ />
+ }
+ />
+ </TransactionTemplate>
+ );
}
if (transaction.type === TransactionType.Payment) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountEffective),
- Amounts.parseOrThrow(transaction.amountRaw),
- ).amount
-
- return <TransactionTemplate>
- <h2>Payment </h2>
- <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
- <br />
- <Part big title="Total paid" text={amountToString(transaction.amountEffective)} kind='negative' />
- <Part big title="Purchase amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
- <Part big title="Fee" text={amountToString(fee)} kind='negative' />
- <Part title="Merchant" text={transaction.info.merchant.name} kind='neutral' />
- <Part title="Purchase" text={transaction.info.summary} kind='neutral' />
- <Part title="Receipt" text={`#${transaction.info.orderId}`} kind='neutral' />
+ const pendingRefund =
+ transaction.refundPending === undefined
+ ? undefined
+ : Amounts.parseOrThrow(transaction.refundPending);
- <div>
- {transaction.info.products && transaction.info.products.length > 0 &&
- <ListOfProducts>
- {transaction.info.products.map((p, k) => <RowBorderGray key={k}>
- <a href="#" onClick={showLargePic}>
- <img src={p.image ? p.image : emptyImg} />
- </a>
+ const effectiveRefund = Amounts.parseOrThrow(
+ transaction.totalRefundEffective,
+ );
+
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onRetry={onRetry}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ total={effective}
+ type={i18n.str`Payment`}
+ kind="negative"
+ >
+ {transaction.info.fulfillmentUrl ? (
+ <a
+ href={transaction.info.fulfillmentUrl}
+ target="_bank"
+ rel="noreferrer"
+ >
+ {transaction.info.summary}
+ </a>
+ ) : (
+ transaction.info.summary
+ )}
+ </Header>
+ <br />
+ {transaction.refunds.length > 0 ? (
+ <Part
+ title={i18n.str`Refunds`}
+ text={
+ <table>
+ {transaction.refunds.map((r, i) => {
+ return (
+ <tr key={i}>
+ <td>
+ <i18n.Translate>
+ {<Amount value={r.amountEffective} />}{" "}
+ <a
+ href={Pages.balanceTransaction({
+ tid: r.transactionId,
+ })}
+ >
+ was refunded
+ </a>{" "}
+ on{" "}
+ {
+ <Time
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ r.timestamp,
+ )}
+ format="dd MMMM yyyy"
+ />
+ }
+ .
+ </i18n.Translate>
+ </td>
+ </tr>
+ );
+ })}
+ </table>
+ }
+ kind="neutral"
+ />
+ ) : undefined}
+ {pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
+ <InfoBox>
+ {transaction.refundQueryActive ? (
+ <i18n.Translate>Refund is in progress.</i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ Merchant created a refund for this order but was not
+ automatically picked up.
+ </i18n.Translate>
+ )}
+ <Part
+ title={i18n.str`Offer`}
+ text={<Amount value={pendingRefund} />}
+ kind="positive"
+ />
+ {transaction.refundQueryActive ? undefined : (
<div>
- {p.quantity && p.quantity > 0 && <SmallLightText>x {p.quantity} {p.unit}</SmallLightText>}
- <div>{p.description}</div>
+ <div />
+ <div>
+ <Button
+ variant="contained"
+ onClick={safely("refund transaction", () =>
+ onRefund(transaction.transactionId),
+ )}
+ >
+ <i18n.Translate>Accept</i18n.Translate>
+ </Button>
+ </div>
</div>
- </RowBorderGray>)}
- </ListOfProducts>
- }
- </div>
- </TransactionTemplate>
+ )}
+ </InfoBox>
+ )}
+ {transaction.posConfirmation ? (
+ <AlertView
+ alert={{
+ type: "info",
+ message: i18n.str`Confirmation code`,
+ description: <pre>{transaction.posConfirmation}</pre>,
+ }}
+ />
+ ) : undefined}
+ <Part
+ title={i18n.str`Merchant`}
+ text={<MerchantDetails merchant={transaction.info.merchant} />}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Invoice ID`}
+ text={transaction.info.orderId as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <PurchaseDetails
+ price={getAmountWithFee(effective, raw, "debit")}
+ effectiveRefund={effectiveRefund}
+ info={transaction.info}
+ />
+ }
+ kind="neutral"
+ />
+ <ShowFullContractTermPopup transactionId={transaction.transactionId} />
+ </TransactionTemplate>
+ );
}
if (transaction.type === TransactionType.Deposit) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountRaw),
- Amounts.parseOrThrow(transaction.amountEffective),
- ).amount
- return <TransactionTemplate>
- <h2>Deposit </h2>
- <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
- <br />
- <Part big title="Total deposit" text={amountToString(transaction.amountEffective)} kind='negative' />
- <Part big title="Purchase amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
- <Part big title="Fee" text={amountToString(fee)} kind='negative' />
- </TransactionTemplate>
+ const payto = parsePaytoUri(transaction.targetPaytoUri);
+
+ const wireTime = AbsoluteTime.fromProtocolTimestamp(
+ transaction.wireTransferDeadline,
+ );
+ const shouldBeWired = wireTime.t_ms !== "never" && isPast(wireTime.t_ms);
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Deposit`}
+ total={effective}
+ kind="negative"
+ >
+ {!payto ? transaction.targetPaytoUri : <NicePayto payto={payto} />}
+ </Header>
+ {payto && <PartPayto payto={payto} kind="neutral" />}
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <DepositDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
+ />
+ }
+ kind="neutral"
+ />
+ {!shouldBeWired ? (
+ <Part
+ title={i18n.str`Wire transfer deadline.`}
+ text={
+ <Time timestamp={wireTime} format="dd MMMM yyyy 'at' HH:mm" />
+ }
+ kind="neutral"
+ />
+ ) : transaction.wireTransferProgress === 0 ? (
+ <AlertView
+ alert={{
+ type: "warning",
+ message: i18n.str`Wire transfer is not initiated.`,
+ description: i18n.str` `,
+ }}
+ />
+ ) : transaction.wireTransferProgress === 100 ? (
+ <Fragment>
+ <AlertView
+ alert={{
+ type: "success",
+ message: i18n.str`Wire transfer completed.`,
+ description: i18n.str` `,
+ }}
+ />
+ <Part
+ title={i18n.str`Transfer details`}
+ text={
+ <TrackingDepositDetails
+ trackingState={transaction.trackingState}
+ />
+ }
+ kind="neutral"
+ />
+ </Fragment>
+ ) : (
+ <AlertView
+ alert={{
+ type: "info",
+ message: i18n.str`Wire transfer in progress.`,
+ description: i18n.str` `,
+ }}
+ />
+ )}
+ </TransactionTemplate>
+ );
}
if (transaction.type === TransactionType.Refresh) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountRaw),
- Amounts.parseOrThrow(transaction.amountEffective),
- ).amount
- return <TransactionTemplate>
- <h2>Refresh</h2>
- <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
- <br />
- <Part big title="Total refresh" text={amountToString(transaction.amountEffective)} kind='negative' />
- <Part big title="Refresh amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
- <Part big title="Fee" text={amountToString(fee)} kind='negative' />
- </TransactionTemplate>
- }
-
- if (transaction.type === TransactionType.Tip) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountRaw),
- Amounts.parseOrThrow(transaction.amountEffective),
- ).amount
- return <TransactionTemplate>
- <h2>Tip</h2>
- <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
- <br />
- <Part big title="Total tip" text={amountToString(transaction.amountEffective)} kind='positive' />
- <Part big title="Received amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
- <Part big title="Fee" text={amountToString(fee)} kind='negative' />
- </TransactionTemplate>
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Refresh`}
+ total={effective}
+ kind="negative"
+ >
+ {"Refresh"}
+ </Header>
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <RefreshDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
+ />
+ }
+ />
+ </TransactionTemplate>
+ );
}
if (transaction.type === TransactionType.Refund) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountRaw),
- Amounts.parseOrThrow(transaction.amountEffective),
- ).amount
- return <TransactionTemplate>
- <h2>Refund</h2>
- <div>{transaction.timestamp.t_ms === 'never' ? 'never' : format(transaction.timestamp.t_ms, 'dd MMMM yyyy, HH:mm')}</div>
- <br />
- <Part big title="Total refund" text={amountToString(transaction.amountEffective)} kind='positive' />
- <Part big title="Refund amount" text={amountToString(transaction.amountRaw)} kind='neutral' />
- <Part big title="Fee" text={amountToString(fee)} kind='negative' />
- <Part title="Merchant" text={transaction.info.merchant.name} kind='neutral' />
- <Part title="Purchase" text={transaction.info.summary} kind='neutral' />
- <Part title="Receipt" text={`#${transaction.info.orderId}`} kind='neutral' />
-
- <p>
- {transaction.info.summary}
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Refund`}
+ total={effective}
+ kind="positive"
+ >
+ {transaction.paymentInfo ? (
+ <a
+ href={Pages.balanceTransaction({
+ tid: transaction.refundedTransactionId,
+ })}
+ >
+ {transaction.paymentInfo.summary}
+ </a>
+ ) : (
+ <span style={{ color: "gray" }}>-- deleted --</span>
+ )}
+ </Header>
+
+ <Part
+ title={i18n.str`Merchant`}
+ text={
+ (transaction.paymentInfo
+ ? transaction.paymentInfo.merchant.name
+ : "-- deleted --") as TranslatedString
+ }
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Purchase summary`}
+ text={
+ (transaction.paymentInfo
+ ? transaction.paymentInfo.summary
+ : "-- deleted --") as TranslatedString
+ }
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <RefundDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
+ />
+ }
+ />
+ </TransactionTemplate>
+ );
+ }
+
+ if (transaction.type === TransactionType.PeerPullCredit) {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Credit`}
+ total={effective}
+ kind="positive"
+ >
+ <i18n.Translate>Invoice</i18n.Translate>
+ </Header>
+
+ {transaction.info.summary ? (
+ <Part
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
+ kind="neutral"
+ />
+ ) : undefined}
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ {transaction.txState.major === TransactionMajorState.Pending &&
+ transaction.txState.minor === TransactionMinorState.Ready &&
+ transaction.talerUri &&
+ !transaction.error && (
+ <Part
+ title={i18n.str`URI`}
+ text={<ShowQrWithCopy text={transaction.talerUri} />}
+ kind="neutral"
+ />
+ )}
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <InvoiceCreationDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
+ />
+ }
+ />
+ </TransactionTemplate>
+ );
+ }
+
+ if (transaction.type === TransactionType.PeerPullDebit) {
+ 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>Invoice</i18n.Translate>
+ </Header>
+
+ {transaction.info.summary ? (
+ <Part
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
+ kind="neutral"
+ />
+ ) : undefined}
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <InvoicePaymentDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
+ />
+ }
+ />
+ </TransactionTemplate>
+ );
+ }
+
+ if (transaction.type === TransactionType.PeerPushDebit) {
+ const total = Amounts.parseOrThrow(transaction.amountEffective);
+ 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={total}
+ kind="negative"
+ >
+ <i18n.Translate>Transfer</i18n.Translate>
+ </Header>
+
+ {transaction.info.summary ? (
+ <Part
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
+ kind="neutral"
+ />
+ ) : undefined}
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ {transaction.talerUri && (
+ <Part
+ title={i18n.str`URI`}
+ text={<ShowQrWithCopy text={transaction.talerUri} />}
+ kind="neutral"
+ />
+ )}
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <TransferCreationDetails
+ amount={getAmountWithFee(effective, raw, "debit")}
+ />
+ }
+ />
+ </TransactionTemplate>
+ );
+ }
+
+ if (transaction.type === TransactionType.PeerPushCredit) {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Credit`}
+ total={effective}
+ kind="positive"
+ >
+ <i18n.Translate>Transfer</i18n.Translate>
+ </Header>
+
+ {transaction.info.summary ? (
+ <Part
+ title={i18n.str`Subject`}
+ text={transaction.info.summary as TranslatedString}
+ kind="neutral"
+ />
+ ) : undefined}
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Details`}
+ text={
+ <TransferPickupDetails
+ amount={getAmountWithFee(effective, raw, "credit")}
+ />
+ }
+ />
+ </TransactionTemplate>
+ );
+ }
+
+ if (transaction.type === TransactionType.DenomLoss) {
+ switch (transaction.lossEventType) {
+ case DenomLossEventType.DenomExpired: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination expired.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ case DenomLossEventType.DenomVanished: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination vanished.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ case DenomLossEventType.DenomUnoffered: {
+ return (
+ <TransactionTemplate
+ transaction={transaction}
+ onDelete={onDelete}
+ onRetry={onRetry}
+ onAbort={onAbort}
+ onResume={onResume}
+ onSuspend={onSuspend}
+ onCancel={onCancel}
+ >
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Debit`}
+ total={effective}
+ kind="negative"
+ >
+ <i18n.Translate>Lost</i18n.Translate>
+ </Header>
+
+ <Part
+ title={i18n.str`Exchange`}
+ text={transaction.exchangeBaseUrl as TranslatedString}
+ kind="neutral"
+ />
+ <Part
+ title={i18n.str`Reason`}
+ text={i18n.str`Denomination is unoffered.`}
+ />
+ </TransactionTemplate>
+ );
+ }
+ default: {
+ assertUnreachable(transaction.lossEventType);
+ }
+ }
+ }
+ if (transaction.type === TransactionType.Recoup) {
+ throw Error("recoup transaction not implemented");
+ }
+ assertUnreachable(transaction);
+}
+
+export function MerchantDetails({
+ merchant,
+}: {
+ merchant: MerchantInfo;
+}): VNode {
+ return (
+ <div style={{ display: "flex", flexDirection: "row" }}>
+ {merchant.logo && (
+ <div>
+ <img
+ src={merchant.logo}
+ style={{ width: 64, height: 64, margin: 4 }}
+ />
+ </div>
+ )}
+ <div>
+ <p style={{ marginTop: 0 }}>{merchant.name}</p>
+ {merchant.website && (
+ <a
+ href={merchant.website}
+ target="_blank"
+ style={{ textDecorationColor: "gray" }}
+ rel="noreferrer"
+ >
+ <SmallLightText>{merchant.website}</SmallLightText>
+ </a>
+ )}
+ {merchant.email && (
+ <a
+ href={`mailto:${merchant.email}`}
+ style={{ textDecorationColor: "gray" }}
+ >
+ <SmallLightText>{merchant.email}</SmallLightText>
+ </a>
+ )}
+ </div>
+ </div>
+ );
+}
+
+export function ExchangeDetails({ exchange }: { exchange: string }): VNode {
+ return (
+ <div>
+ <p style={{ marginTop: 0 }}>
+ <a rel="noreferrer" target="_blank" href={exchange}>
+ {exchange}
+ </a>
</p>
+ </div>
+ );
+}
+
+export interface AmountWithFee {
+ value: AmountJson;
+ fee: AmountJson;
+ total: AmountJson;
+ maxFrac: number;
+}
+
+export function getAmountWithFee(
+ effective: AmountJson,
+ raw: AmountJson,
+ direction: "credit" | "debit",
+): AmountWithFee {
+ const total = direction === "credit" ? effective : raw;
+ const value = direction === "debit" ? effective : raw;
+ const fee = Amounts.sub(value, total).amount;
+
+ const maxFrac = [effective, raw, fee]
+ .map((a) => Amounts.maxFractionalDigits(a))
+ .reduce((c, p) => Math.max(c, p), 0);
+
+ return {
+ total,
+ value,
+ fee,
+ maxFrac,
+ };
+}
+
+export function InvoiceCreationDetails({
+ amount,
+}: {
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>
+ <i18n.Translate>Invoice</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+export function InvoicePaymentDetails({
+ amount,
+}: {
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>
+ <i18n.Translate>Invoice</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+export function TransferCreationDetails({
+ amount,
+}: {
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>
+ <i18n.Translate>Sent</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Transfer</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+export function TransferPickupDetails({
+ amount,
+}: {
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>
+ <i18n.Translate>Transfer</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+export function WithdrawDetails({
+ conversion,
+ amount,
+}: {
+ conversion?: AmountJson;
+ amount: AmountWithFee;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
+ {conversion ? (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Transfer</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={conversion} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ {conversion.fraction === amount.value.fraction &&
+ conversion.value === amount.value.value ? undefined : (
+ <tr>
+ <td>
+ <i18n.Translate>Converted</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ )}
+ </Fragment>
+ ) : (
+ <tr>
+ <td>
+ <i18n.Translate>Transfer</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ )}
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+export function PurchaseDetails({
+ price,
+ effectiveRefund,
+ info: _info,
+}: {
+ price: AmountWithFee;
+ effectiveRefund?: AmountJson;
+ info: OrderShortInfo;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const total = Amounts.add(price.value, price.fee).amount;
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>
+ <i18n.Translate>Price</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.total} />
+ </td>
+ </tr>
+ {Amounts.isNonZero(price.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.fee} />
+ </td>
+ </tr>
+ {effectiveRefund && Amounts.isNonZero(effectiveRefund) ? (
+ <Fragment>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Subtotal</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.total} />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Refunded</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={effectiveRefund} negative />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={Amounts.sub(total, effectiveRefund).amount} />
+ </td>
+ </tr>
+ </Fragment>
+ ) : (
+ <Fragment>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={price.value} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </Fragment>
+ )}
+
+ {/* {hasProducts && (
+ <tr>
+ <td colSpan={2}>
+ <PartCollapsible
+ big
+ title={i18n.str`Products`}
+ text={
+ <ListOfProducts>
+ {info.products?.map((p, k) => (
+ <Row key={k}>
+ <a href="#" onClick={showLargePic}>
+ <img src={p.image ? p.image : emptyImg} />
+ </a>
+ <div>
+ {p.quantity && p.quantity > 0 && (
+ <SmallLightText>
+ x {p.quantity} {p.unit}
+ </SmallLightText>
+ )}
+ <div>{p.description}</div>
+ </div>
+ </Row>
+ ))}
+ </ListOfProducts>
+ }
+ />
+ </td>
+ </tr>
+ )} */}
+ {/* {hasShipping && (
+ <tr>
+ <td colSpan={2}>
+ <PartCollapsible
+ big
+ title={i18n.str`Delivery`}
+ text={
+ <DeliveryDetails
+ date={info.delivery_date}
+ location={info.delivery_location}
+ />
+ }
+ />
+ </td>
+ </tr>
+ )} */}
+ </PurchaseDetailsTable>
+ );
+}
+
+function RefundDetails({ amount }: { amount: AmountWithFee }): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>
+ <i18n.Translate>Refund</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+type AmountAmountByWireTransferByWire = {
+ id: string;
+ amount: AmountString;
+}[];
+
+function calculateAmountByWireTransfer(
+ state: TransactionDeposit["trackingState"],
+): AmountAmountByWireTransferByWire {
+ const allTracking = Object.values(state ?? {});
+
+ //group tracking by wtid, sum amounts
+ const trackByWtid = allTracking.reduce(
+ (prev, cur) => {
+ const fee = Amounts.parseOrThrow(cur.wireFee);
+ const raw = Amounts.parseOrThrow(cur.amountRaw);
+ const total = !prev[cur.wireTransferId]
+ ? raw
+ : Amounts.add(prev[cur.wireTransferId].total, raw).amount;
+
+ prev[cur.wireTransferId] = {
+ total,
+ fee,
+ };
+ return prev;
+ },
+ {} as Record<string, { total: AmountJson; fee: AmountJson }>,
+ );
+
+ //remove wire fee from total amount
+ return Object.entries(trackByWtid).map(([id, info]) => ({
+ id,
+ amount: Amounts.stringify(Amounts.sub(info.total, info.fee).amount),
+ }));
+}
+
+function TrackingDepositDetails({
+ trackingState,
+}: {
+ trackingState: TransactionDeposit["trackingState"];
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const wireTransfers = calculateAmountByWireTransfer(trackingState);
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>
+ <i18n.Translate>Transfer identification</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>Amount</i18n.Translate>
+ </td>
+ </tr>
+
+ {wireTransfers.map((wire) => (
+ <tr key={wire.id}>
+ <td>{wire.id}</td>
+ <td>
+ <Amount value={wire.amount} />
+ </td>
+ </tr>
+ ))}
+ </PurchaseDetailsTable>
+ );
+}
+
+function DepositDetails({ amount }: { amount: AmountWithFee }): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>
+ <i18n.Translate>Sent</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(amount.fee) && (
+ <Fragment>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Total</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.total} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+function RefreshDetails({ amount }: { amount: AmountWithFee }): VNode {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>
+ <i18n.Translate>Refresh</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.value} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <i18n.Translate>Fees</i18n.Translate>
+ </td>
+ <td>
+ <Amount value={amount.fee} maxFracSize={amount.maxFrac} />
+ </td>
+ </tr>
+ <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>
+ );
+}
+
+function Header({
+ timestamp,
+ total,
+ children,
+ kind,
+ type,
+}: {
+ timestamp: TalerPreciseTimestamp;
+ total: AmountJson;
+ children: ComponentChildren;
+ kind: Kind;
+ type: TranslatedString;
+}): VNode {
+ return (
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-between",
+ flexDirection: "row",
+ }}
+ >
<div>
- {transaction.info.products && transaction.info.products.length > 0 &&
- <ListOfProducts>
- {transaction.info.products.map((p, k) => <RowBorderGray key={k}>
- <a href="#" onClick={showLargePic}>
- <img src={p.image ? p.image : emptyImg} />
+ <SubTitle>{children}</SubTitle>
+ <Time
+ timestamp={AbsoluteTime.fromPreciseTimestamp(timestamp)}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ </div>
+ <div>
+ <SubTitle>
+ <Part
+ title={type}
+ text={<Amount value={total} negative={kind === "negative"} />}
+ kind={kind}
+ />
+ </SubTitle>
+ </div>
+ </div>
+ );
+}
+
+function NicePayto({ payto }: { payto: PaytoUri }): VNode {
+ if (payto.isKnown) {
+ switch (payto.targetType) {
+ case "bitcoin": {
+ return <div>{payto.targetPath.substring(0, 20)}...</div>;
+ }
+ case "x-taler-bank": {
+ const url = new URL("/", `https://${payto.host}`);
+ return (
+ <Fragment>
+ <div>{"payto.account"}</div>
+ <SmallLightText>
+ <a href={url.href} target="_bank" rel="noreferrer">
+ {url.href}
</a>
- <div>
- {p.quantity && p.quantity > 0 && <SmallLightText>x {p.quantity} {p.unit}</SmallLightText>}
- <div>{p.description}</div>
- </div>
- </RowBorderGray>)}
- </ListOfProducts>
- }
+ </SmallLightText>
+ </Fragment>
+ );
+ }
+ case "iban": {
+ return <div>{payto.targetPath.substring(0, 20)}</div>;
+ }
+ }
+ }
+ return <Fragment>{stringifyPaytoUri(payto)}</Fragment>;
+}
+
+function ShowQrWithCopy({ text }: { text: string }): VNode {
+ const [showing, setShowing] = useState(false);
+ const { i18n } = useTranslationContext();
+ async function copy(): Promise<void> {
+ navigator.clipboard.writeText(text);
+ }
+ async function toggle(): Promise<void> {
+ setShowing((s) => !s);
+ }
+ if (showing) {
+ return (
+ <div>
+ <QR text={text} />
+ <Button onClick={copy as SafeHandler<void>}>
+ <i18n.Translate>copy</i18n.Translate>
+ </Button>
+ <Button onClick={toggle as SafeHandler<void>}>
+ <i18n.Translate>hide qr</i18n.Translate>
+ </Button>
</div>
- </TransactionTemplate>
+ );
}
+ return (
+ <div>
+ <div>{text.substring(0, 64)}...</div>
+ <Button onClick={copy as SafeHandler<void>}>
+ <i18n.Translate>copy</i18n.Translate>
+ </Button>
+ <Button onClick={toggle as SafeHandler<void>}>
+ <i18n.Translate>show qr</i18n.Translate>
+ </Button>
+ </div>
+ );
+}
+function getShowButtonStates(transaction: Transaction) {
+ let abort = false;
+ let fail = false;
+ let resume = false;
+ let remove = false;
+ let suspend = false;
+
+ transaction.txActions.forEach((a) => {
+ switch (a) {
+ case TransactionAction.Delete:
+ remove = true;
+ break;
+ case TransactionAction.Suspend:
+ suspend = true;
+ break;
+ case TransactionAction.Resume:
+ resume = true;
+ break;
+ case TransactionAction.Abort:
+ abort = true;
+ break;
+ case TransactionAction.Fail:
+ fail = true;
+ break;
+ case TransactionAction.Retry:
+ break;
+ default:
+ assertUnreachable(a);
+ break;
+ }
+ });
+ return { abort, fail, resume, remove, suspend };
+}
+
+function ShowWithdrawalDetailForBankIntegrated({
+ transaction,
+}: {
+ transaction: TransactionWithdrawal | TransactionInternalWithdrawal;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [showDetails, setShowDetails] = useState(false);
+ if (
+ transaction.txState.major !== TransactionMajorState.Pending ||
+ transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
+ ) {
+ return <Fragment />;
+ }
+ const raw = Amounts.parseOrThrow(transaction.amountRaw);
+ return (
+ <Fragment>
+ <EnabledBySettings name="advancedMode">
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setShowDetails(!showDetails);
+ }}
+ >
+ Show details.
+ </a>
+ </EnabledBySettings>
- return <div></div>
+ {showDetails && (
+ <BankDetailsByPaytoType
+ amount={raw}
+ accounts={
+ transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
+ }
+ subject={transaction.withdrawalDetails.reservePub}
+ />
+ )}
+ {!transaction.withdrawalDetails.confirmed &&
+ transaction.withdrawalDetails.bankConfirmationUrl ? (
+ <InfoBox>
+ <div style={{ display: "block" }}>
+ <i18n.Translate>
+ Wire transfer need a confirmation. Go to the{" "}
+ <a
+ href={transaction.withdrawalDetails.bankConfirmationUrl}
+ target="_blank"
+ rel="noreferrer"
+ style={{ display: "inline" }}
+ >
+ <i18n.Translate>bank site</i18n.Translate>
+ </a>{" "}
+ and check wire transfer operation to exchange account is complete.
+ </i18n.Translate>
+ </div>
+ </InfoBox>
+ ) : undefined}
+ {transaction.withdrawalDetails.confirmed &&
+ !transaction.withdrawalDetails.reserveIsReady && (
+ <InfoBox>
+ <i18n.Translate>
+ Bank has confirmed the wire transfer. Waiting for the exchange to
+ send the coins.
+ </i18n.Translate>
+ </InfoBox>
+ )}
+ {transaction.withdrawalDetails.confirmed &&
+ transaction.withdrawalDetails.reserveIsReady && (
+ <InfoBox>
+ <i18n.Translate>
+ Exchange is ready to send the coins, withdrawal in progress.
+ </i18n.Translate>
+ </InfoBox>
+ )}
+ </Fragment>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
index 6579450b3..dfce1c14b 100644
--- a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -15,36 +15,26 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample } from '../test-utils';
-import { View as TestedComponent } from './Welcome';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import * as tests from "@gnu-taler/web-util/testing";
+import { View as TestedComponent } from "./Welcome.js";
export default {
- title: 'wallet/welcome',
+ title: "welcome",
component: TestedComponent,
};
-export const Normal = createExample(TestedComponent, {
- permissionsEnabled: true,
- diagnostics: {
- errors: [],
- walletManifestVersion: '1.0',
- walletManifestDisplayVersion: '1.0',
- firefoxIdbProblem: false,
- dbOutdated: false,
- }
+export const Normal = tests.createExample(TestedComponent, {
+ permissionToggle: { value: true, button: {} },
});
-export const TimedoutDiagnostics = createExample(TestedComponent, {
- timedOut: true,
- permissionsEnabled: false,
+export const TimedoutDiagnostics = tests.createExample(TestedComponent, {
+ permissionToggle: { value: true, button: {} },
});
-export const RunningDiagnostics = createExample(TestedComponent, {
- permissionsEnabled: false,
+export const RunningDiagnostics = tests.createExample(TestedComponent, {
+ permissionToggle: { value: true, button: {} },
});
-
diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
index d11070d9a..6a57fe18a 100644
--- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 Taler Systems SA
+ (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
@@ -17,53 +17,82 @@
/**
* Welcome page, shown on first installs.
*
- * @author Florian Dold
+ * @author sebasjm
*/
-import { JSX } from "preact/jsx-runtime";
-import { Checkbox } from "../components/Checkbox";
-import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
-import { Diagnostics } from "../components/Diagnostics";
-import { WalletBox } from "../components/styled";
-import { useDiagnostics } from "../hooks/useDiagnostics";
import { WalletDiagnostics } from "@gnu-taler/taler-util";
-import { h } from 'preact';
+import { Fragment, h, VNode } from "preact";
+import { Checkbox } from "../components/Checkbox.js";
+import { SubTitle, Title } from "../components/styled/index.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useSettings } from "../hooks/useSettings.js";
+import { ToggleHandler } from "../mui/handlers.js";
+import { platform } from "../platform/foreground.js";
+import { useAlertContext } from "../context/alert.js";
-export function WelcomePage() {
- const [permissionsEnabled, togglePermissions] = useExtendedPermissions()
- const [diagnostics, timedOut] = useDiagnostics()
- return <View
- permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions}
- diagnostics={diagnostics} timedOut={timedOut}
- />
+export function WelcomePage(): VNode {
+ const [settings, updateSettings] = useSettings();
+ const { safely } = useAlertContext();
+ return (
+ <View
+ permissionToggle={{
+ value: settings.injectTalerSupport,
+ button: {
+ onClick: safely("update support injection", async () =>
+ updateSettings("injectTalerSupport", !settings.injectTalerSupport),
+ ),
+ },
+ }}
+ />
+ );
}
export interface ViewProps {
- permissionsEnabled: boolean,
- togglePermissions: () => void,
- diagnostics: WalletDiagnostics | undefined,
- timedOut: boolean,
+ permissionToggle: ToggleHandler;
}
-export function View({ permissionsEnabled, togglePermissions, diagnostics, timedOut }: ViewProps): JSX.Element {
- return (<WalletBox>
- <h1>Browser Extension Installed!</h1>
- <div>
- <p>Thank you for installing the wallet.</p>
- <Diagnostics diagnostics={diagnostics} timedOut={timedOut} />
- <h2>Permissions</h2>
- <Checkbox label="Automatically open wallet based on page content"
- name="perm"
- description="(Enabling this option below will make using the wallet faster, but requires more permissions from your browser.)"
- enabled={permissionsEnabled} onToggle={togglePermissions}
- />
- <h2>Next Steps</h2>
- <a href="https://demo.taler.net/" style={{ display: "block" }}>
- Try the demo »
- </a>
- <a href="https://demo.taler.net/" style={{ display: "block" }}>
- Learn how to top up your wallet balance »
- </a>
- </div>
- </WalletBox>
+export function View({
+ permissionToggle,
+}: ViewProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <Title>
+ <i18n.Translate>GNU Taler Wallet installed!</i18n.Translate>
+ </Title>
+ <div>
+ <p>
+ <i18n.Translate>
+ You can open the wallet using the combination{" "}
+ <pre style="font-weight: bold; display: inline;">&lt;ALT+W&gt;</pre>
+ .
+ </i18n.Translate>
+ </p>
+ <Fragment>
+ <p>
+ <i18n.Translate>
+ Also pinning the GNU Taler Wallet to your browser allows
+ you to quick access without keyboard:
+ </i18n.Translate>
+ </p>
+ <ol style={{ paddingLeft: 40 }}>
+ <li>
+ <i18n.Translate>Click the puzzle icon</i18n.Translate>
+ </li>
+ <li>
+ <i18n.Translate>Search for GNU Taler Wallet</i18n.Translate>
+ </li>
+ <li>
+ <i18n.Translate>Click the pin icon</i18n.Translate>
+ </li>
+ </ol>
+ </Fragment>
+ <SubTitle>
+ <i18n.Translate>Next Steps</i18n.Translate>
+ </SubTitle>
+ <a href="https://demo.taler.net/" style={{ display: "block" }}>
+ <i18n.Translate>Try the demo</i18n.Translate> »
+ </a>
+ </div>
+ </Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/wallet/index.stories.tsx b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
new file mode 100644
index 000000000..89bb75b29
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/index.stories.tsx
@@ -0,0 +1,36 @@
+/*
+ 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)
+ */
+
+export * as a1 from "./Backup.stories.js";
+export * as a4 from "./DepositPage/stories.js";
+export * as a7 from "./History.stories.js";
+export * as a8 from "./AddBackupProvider/stories.js";
+export * as a10 from "./ProviderDetail.stories.js";
+export * as a12 from "./Settings.stories.js";
+export * as a13 from "./Transaction.stories.js";
+export * as a14 from "./Welcome.stories.js";
+export * as a15 from "./AddNewActionView.stories.js";
+export * as a16 from "./DeveloperPage.stories.js";
+export * as a17 from "./QrReader.stories.js";
+export * as a18 from "./DestinationSelection/stories.js";
+export * as a19 from "./ExchangeSelection/stories.js";
+export * as a20 from "./ManageAccount/stories.js";
+export * as a21 from "./Notifications/stories.js";
diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.dev.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.dev.tsx
new file mode 100644
index 000000000..60a5970e4
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/walletEntryPoint.dev.tsx
@@ -0,0 +1,53 @@
+/*
+ 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/>
+ */
+
+/**
+ * Main entry point for extension pages.
+ *
+ * @author sebasjm
+ */
+
+import { setupI18n } from "@gnu-taler/taler-util";
+import { h, render } from "preact";
+import { strings } from "./i18n/strings.js";
+import { setupPlatform } from "./platform/foreground.js";
+import devAPI from "./platform/dev.js";
+import { Application } from "./wallet/Application.js";
+
+setupPlatform(devAPI);
+
+function main(): void {
+ try {
+ const container = document.getElementById("container");
+ if (!container) {
+ throw Error("container not found, can't mount page contents");
+ }
+ render(<Application />, container);
+ } catch (e) {
+ console.error("got error", e);
+ if (e instanceof Error) {
+ document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
+ }
+ }
+}
+
+setupI18n("en", strings);
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
index 023ee94c5..1bd42796b 100644
--- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -17,33 +17,26 @@
/**
* Main entry point for extension pages.
*
- * @author Florian Dold <dold@taler.net>
+ * @author sebasjm
*/
import { setupI18n } from "@gnu-taler/taler-util";
-import { createHashHistory } from 'history';
-import { Fragment, h, render } from "preact";
-import Router, { route, Route } from "preact-router";
-import { useEffect } from "preact/hooks";
-import { LogoHeader } from "./components/LogoHeader";
-import { DevContextProvider } from "./context/devContext";
-import { PayPage } from "./cta/Pay";
-import { RefundPage } from "./cta/Refund";
-import { TipPage } from './cta/Tip';
-import { WithdrawPage } from "./cta/Withdraw";
-import { strings } from "./i18n/strings";
-import {
- Pages, WalletNavBar
-} from "./NavigationBar";
-import { BalancePage } from "./wallet/BalancePage";
-import { HistoryPage } from "./wallet/History";
-import { SettingsPage } from "./wallet/Settings";
-import { TransactionPage } from './wallet/Transaction';
-import { WelcomePage } from "./wallet/Welcome";
-import { BackupPage } from './wallet/BackupPage';
-import { DeveloperPage } from "./popup/Debug.js";
-import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage.js";
-
+import { h, render } from "preact";
+import { strings } from "./i18n/strings.js";
+import { setupPlatform } from "./platform/foreground.js";
+import chromeAPI from "./platform/chrome.js";
+import firefoxAPI from "./platform/firefox.js";
+import { Application } from "./wallet/Application.js";
+
+const isFirefox = typeof (window as any)["InstallTrigger"] !== "undefined";
+
+//FIXME: create different entry point for any platform instead of
+//switching in runtime
+if (isFirefox) {
+ setupPlatform(firefoxAPI);
+} else {
+ setupPlatform(chromeAPI);
+}
function main(): void {
try {
@@ -60,60 +53,10 @@ function main(): void {
}
}
-setupI18n("en-US", strings);
+setupI18n("en", strings);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", main);
} else {
main();
}
-
-function withLogoAndNavBar(Component: any) {
- return (props: any) => <Fragment>
- <LogoHeader />
- <WalletNavBar />
- <Component {...props} />
- </Fragment>
-}
-
-function Application() {
- return <div>
- <DevContextProvider>
- <Router history={createHashHistory()} >
-
- <Route path={Pages.welcome} component={withLogoAndNavBar(WelcomePage)} />
-
- <Route path={Pages.history} component={withLogoAndNavBar(HistoryPage)} />
- <Route path={Pages.transaction} component={withLogoAndNavBar(TransactionPage)} />
- <Route path={Pages.balance} component={withLogoAndNavBar(BalancePage)}
- goToWalletManualWithdraw={() => route(Pages.manual_withdraw)}
- />
- <Route path={Pages.settings} component={withLogoAndNavBar(SettingsPage)} />
- <Route path={Pages.backup} component={withLogoAndNavBar(BackupPage)} />
-
- <Route path={Pages.manual_withdraw} component={withLogoAndNavBar(ManualWithdrawPage)} />
-
- <Route path={Pages.reset_required} component={() => <div>no yet implemented</div>} />
- <Route path={Pages.payback} component={() => <div>no yet implemented</div>} />
- <Route path={Pages.return_coins} component={() => <div>no yet implemented</div>} />
-
- <Route path={Pages.dev} component={withLogoAndNavBar(DeveloperPage)} />
-
- {/** call to action */}
- <Route path={Pages.pay} component={PayPage} />
- <Route path={Pages.refund} component={RefundPage} />
- <Route path={Pages.tips} component={TipPage} />
- <Route path={Pages.withdraw} component={WithdrawPage} />
-
- <Route default component={Redirect} to={Pages.history} />
- </Router>
- </DevContextProvider>
- </div>
-}
-
-function Redirect({ to }: { to: string }): null {
- useEffect(() => {
- route(to, true)
- })
- return null
-}
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index 92597cbd2..4394a982f 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -1,17 +1,17 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
@@ -22,343 +22,218 @@
* Imports.
*/
import {
+ AbsoluteTime,
CoreApiResponse,
- ConfirmPayResult,
- BalancesResponse,
- TransactionsResponse,
- ApplyRefundResponse,
- PreparePayResult,
- AcceptWithdrawalResponse,
- WalletDiagnostics,
- GetWithdrawalDetailsForUriRequest,
- WithdrawUriInfoResponse,
- PrepareTipRequest,
- PrepareTipResult,
- AcceptTipRequest,
- DeleteTransactionRequest,
- RetryTransactionRequest,
- SetWalletDeviceIdRequest,
- GetExchangeWithdrawalInfo,
- AcceptExchangeTosRequest,
- AcceptManualWithdrawalResult,
- AcceptManualWithdrawalRequest,
- AmountJson,
- ExchangesListRespose,
- AddExchangeRequest,
- GetExchangeTosResult,
+ DetailsMap,
+ Logger,
+ LogLevel,
+ NotificationType,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ WalletNotification
} from "@gnu-taler/taler-util";
-import { AddBackupProviderRequest, BackupProviderState, OperationFailedError, RemoveBackupProviderRequest } from "@gnu-taler/taler-wallet-core";
-import { BackupInfo } from "@gnu-taler/taler-wallet-core";
-import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
-
-export interface ExtendedPermissionsResponse {
- newValue: boolean;
-}
-
-/**
- * Response with information about available version upgrades.
- */
-export interface UpgradeResponse {
- /**
- * Is a reset required because of a new DB version
- * that can't be automatically upgraded?
- */
- dbResetRequired: boolean;
-
- /**
- * Current database version.
- */
- currentDbVersion: string;
-
- /**
- * Old db version (if applicable).
- */
- oldDbVersion: string;
-}
-
-async function callBackend(operation: string, payload: any): Promise<any> {
- return new Promise<any>((resolve, reject) => {
- chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => {
- if (chrome.runtime.lastError) {
- console.log("Error calling backend");
- reject(
- new Error(
- `Error contacting backend: chrome.runtime.lastError.message`,
- ),
- );
- }
- console.log("got response", resp);
- const r = resp as CoreApiResponse;
- if (r.type === "error") {
- reject(new OperationFailedError(r.error));
- return;
- }
- resolve(r.result);
- });
- });
-}
-
-/**
- * Start refreshing a coin.
- */
-export function refresh(coinPub: string): Promise<void> {
- return callBackend("refresh-coin", { coinPub });
-}
-
-/**
- * Pay for a proposal.
- */
-export function confirmPay(
- proposalId: string,
- sessionId: string | undefined,
-): Promise<ConfirmPayResult> {
- return callBackend("confirmPay", { proposalId, sessionId });
-}
-
-/**
- * Check upgrade information
- */
-export function checkUpgrade(): Promise<UpgradeResponse> {
- return callBackend("check-upgrade", {});
-}
-
-/**
- * Reset database
- */
-export function resetDb(): Promise<void> {
- return callBackend("reset-db", {});
-}
-
-/**
- * Get balances for all currencies/exchanges.
- */
-export function getBalance(): Promise<BalancesResponse> {
- return callBackend("getBalances", {});
-}
-
-/**
- * Retrieve the full event history for this wallet.
- */
-export function getTransactions(): Promise<TransactionsResponse> {
- return callBackend("getTransactions", {});
-}
-
-interface CurrencyInfo {
- name: string;
- baseUrl: string;
- pub: string;
-}
-interface ListOfKnownCurrencies {
- auditors: CurrencyInfo[],
- exchanges: CurrencyInfo[],
-}
-
-/**
- * Get a list of currencies from known auditors and exchanges
- */
-export function listKnownCurrencies(): Promise<ListOfKnownCurrencies> {
- return callBackend("listCurrencies", {}).then(result => {
- console.log("result list", result)
- const auditors = result.trustedAuditors.map((a: Record<string, string>) => ({
- name: a.currency,
- baseUrl: a.auditorBaseUrl,
- pub: a.auditorPub,
- }))
- const exchanges = result.trustedExchanges.map((a: Record<string, string>) => ({
- name: a.currency,
- baseUrl: a.exchangeBaseUrl,
- pub: a.exchangeMasterPub,
- }))
- return { auditors, exchanges }
- });
-}
-
-export function listExchanges(): Promise<ExchangesListRespose> {
- return callBackend("listExchanges", {})
-}
-
-/**
- * Get information about the current state of wallet backups.
- */
-export function getBackupInfo(): Promise<BackupInfo> {
- return callBackend("getBackupInfo", {})
-}
-
-/**
- * Add a backup provider and activate it
- */
-export function addBackupProvider(backupProviderBaseUrl: string, name: string): Promise<void> {
- return callBackend("addBackupProvider", {
- backupProviderBaseUrl, activate: true, name
- } as AddBackupProviderRequest)
-}
-
-export function setWalletDeviceId(walletDeviceId: string): Promise<void> {
- return callBackend("setWalletDeviceId", {
- walletDeviceId
- } as SetWalletDeviceIdRequest)
-}
-
-export function syncAllProviders(): Promise<void> {
- return callBackend("runBackupCycle", {})
-}
-
-export function syncOneProvider(url: string): Promise<void> {
- return callBackend("runBackupCycle", { providers: [url] })
-}
-export function removeProvider(url: string): Promise<void> {
- return callBackend("removeBackupProvider", { provider: url } as RemoveBackupProviderRequest)
-}
-export function extendedProvider(url: string): Promise<void> {
- return callBackend("extendBackupProvider", { provider: url })
-}
-
-/**
- * Retry a transaction
- * @param transactionId
- * @returns
- */
-export function retryTransaction(transactionId: string): Promise<void> {
- return callBackend("retryTransaction", {
- transactionId
- } as RetryTransactionRequest);
-}
-
-/**
- * Permanently delete a transaction from the transaction list
- */
-export function deleteTransaction(transactionId: string): Promise<void> {
- return callBackend("deleteTransaction", {
- transactionId
- } as DeleteTransactionRequest);
-}
+import {
+ WalletCoreApiClient,
+ WalletCoreOpKeys,
+ WalletCoreRequestType,
+ WalletCoreResponseType,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ ExtensionNotification,
+ MessageFromBackend,
+ MessageFromFrontendBackground,
+ MessageFromFrontendWallet,
+} from "./platform/api.js";
+import { platform } from "./platform/foreground.js";
+import { WalletActivityTrack } from "./wxBackend.js";
/**
- * Download a refund and accept it.
+ *
+ * @author sebasjm
*/
-export function applyRefund(
- talerRefundUri: string,
-): Promise<ApplyRefundResponse> {
- return callBackend("applyRefund", { talerRefundUri });
-}
-/**
- * Get details about a pay operation.
- */
-export function preparePay(talerPayUri: string): Promise<PreparePayResult> {
- return callBackend("preparePay", { talerPayUri });
-}
+const logger = new Logger("wxApi");
-/**
- * Get details about a withdraw operation.
- */
-export function acceptWithdrawal(
- talerWithdrawUri: string,
- selectedExchange: string,
-): Promise<AcceptWithdrawalResponse> {
- return callBackend("acceptBankIntegratedWithdrawal", {
- talerWithdrawUri,
- exchangeBaseUrl: selectedExchange,
- });
-}
+export const WALLET_CORE_SUPPORTED_VERSION = "4:0:0"
-/**
- * Create a reserve into the exchange that expect the amount indicated
- * @param exchangeBaseUrl
- * @param amount
- * @returns
- */
-export function acceptManualWithdrawal(
- exchangeBaseUrl: string,
- amount: string,
-): Promise<AcceptManualWithdrawalResult> {
- return callBackend("acceptManualWithdrawal", {
- amount, exchangeBaseUrl
- });
+export interface ExtendedPermissionsResponse {
+ newValue: boolean;
}
-export function setExchangeTosAccepted(
- exchangeBaseUrl: string,
- etag: string | undefined
-): Promise<void> {
- return callBackend("setExchangeTosAccepted", {
- exchangeBaseUrl, etag
- } as AcceptExchangeTosRequest)
+export interface BackgroundOperations {
+ resetDb: {
+ request: void;
+ response: void;
+ };
+ runGarbageCollector: {
+ request: void;
+ response: void;
+ };
+ reinitWallet: {
+ request: void;
+ response: void;
+ };
+ getNotifications: {
+ request: {
+ filter: string;
+ };
+ response: WalletActivityTrack[];
+ };
+ clearNotifications: {
+ request: void;
+ response: void;
+ };
+ setLoggingLevel: {
+ request: {
+ tag?: string;
+ level: LogLevel;
+ };
+ response: void;
+ };
}
+export type WalletEvent = { notification: WalletNotification, when: AbsoluteTime }
-/**
- * Get diagnostics information
- */
-export function getDiagnostics(): Promise<WalletDiagnostics> {
- return callBackend("wxGetDiagnostics", {});
+export interface BackgroundApiClient {
+ call<Op extends keyof BackgroundOperations>(
+ operation: Op,
+ payload: BackgroundOperations[Op]["request"],
+ ): Promise<BackgroundOperations[Op]["response"]>;
}
-/**
- * Get diagnostics information
- */
-export function setExtendedPermissions(
- value: boolean,
-): Promise<ExtendedPermissionsResponse> {
- return callBackend("wxSetExtendedPermissions", { value });
-}
+export class BackgroundError<T = any> extends Error {
+ public readonly errorDetail: TalerErrorDetail & T;
+ public readonly cause: Error;
-/**
- * Get diagnostics information
- */
-export function getExtendedPermissions(): Promise<ExtendedPermissionsResponse> {
- return callBackend("wxGetExtendedPermissions", {});
-}
+ constructor(title: string, e: TalerErrorDetail & T, cause: Error) {
+ super(title);
+ this.errorDetail = e;
+ this.cause = cause;
+ }
-/**
- * Get diagnostics information
- */
-export function getWithdrawalDetailsForUri(
- req: GetWithdrawalDetailsForUriRequest,
-): Promise<WithdrawUriInfoResponse> {
- return callBackend("getWithdrawalDetailsForUri", req);
+ hasErrorCode<C extends keyof DetailsMap>(
+ code: C,
+ ): this is BackgroundError<DetailsMap[C]> {
+ return this.errorDetail.code === code;
+ }
}
-
/**
- * Get diagnostics information
+ * BackgroundApiClient integration with browser platform
*/
-export function getExchangeWithdrawalInfo(
- req: GetExchangeWithdrawalInfo,
-): Promise<ExchangeWithdrawDetails> {
- return callBackend("getExchangeWithdrawalInfo", req);
-}
-export function getExchangeTos(
- exchangeBaseUrl: string,
- acceptedFormat: string[],
-): Promise<GetExchangeTosResult> {
- return callBackend("getExchangeTos", {
- exchangeBaseUrl, acceptedFormat
- });
-}
-
-export function addExchange(
- req: AddExchangeRequest,
-): Promise<void> {
- return callBackend("addExchange", req);
-}
+class BackgroundApiClientImpl implements BackgroundApiClient {
+ async call<Op extends keyof BackgroundOperations>(
+ operation: Op,
+ payload: BackgroundOperations[Op]["request"],
+ ): Promise<BackgroundOperations[Op]["response"]> {
+ let response: CoreApiResponse;
+ const message: MessageFromFrontendBackground<Op> = {
+ channel: "background",
+ operation,
+ payload,
+ };
-export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> {
- return callBackend("prepareTip", req);
-}
-
-export function acceptTip(req: AcceptTipRequest): Promise<void> {
- return callBackend("acceptTip", req);
+ try {
+ response = await platform.sendMessageToBackground(message);
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new BackgroundError(operation, {
+ code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ when: AbsoluteTime.now(),
+ }, error);
+ }
+ throw error;
+ }
+ if (response.type === "error") {
+ throw new BackgroundError(
+ `Background operation "${operation}" failed`,
+ response.error,
+ TalerError.fromUncheckedDetail(response.error),
+ );
+ }
+ logger.trace("response", response);
+ return response.result as any;
+ }
+}
+
+/**
+ * WalletCoreApiClient integration with browser platform
+ */
+class WalletApiClientImpl implements WalletCoreApiClient {
+ async call<Op extends WalletCoreOpKeys>(
+ operation: Op,
+ payload: WalletCoreRequestType<Op>,
+ ): Promise<WalletCoreResponseType<Op>> {
+ let response: CoreApiResponse;
+ try {
+ const message: MessageFromFrontendWallet<Op> = {
+ channel: "wallet",
+ operation,
+ payload,
+ };
+ response = await platform.sendMessageToBackground(message);
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new BackgroundError(operation, {
+ code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ when: AbsoluteTime.now(),
+ }, error);
+ }
+ throw error;
+ }
+ if (response.type === "error") {
+ throw new BackgroundError(
+ `Wallet operation "${operation}" failed`,
+ response.error,
+ TalerError.fromUncheckedDetail(response.error)
+ );
+ }
+ logger.trace("got response", response);
+ return response.result as any;
+ }
+}
+
+function onUpdateNotification(
+ messageTypes: Array<NotificationType>,
+ doCallback: undefined | ((n: WalletNotification) => void),
+): () => void {
+ //if no callback, then ignore
+ if (!doCallback)
+ return () => {
+ return;
+ };
+ const onNewMessage = (message: MessageFromBackend): void => {
+ const shouldNotify = message.type === "wallet" && messageTypes.includes(message.notification.type);
+ if (shouldNotify) {
+ doCallback(message.notification);
+ }
+ };
+ return platform.listenToWalletBackground(onNewMessage);
}
-export function onUpdateNotification(f: () => void): () => void {
- const port = chrome.runtime.connect({ name: "notifications" });
- const listener = (): void => {
- f();
- };
- port.onMessage.addListener(listener);
- return () => {
- port.onMessage.removeListener(listener);
+export type WxApiType = {
+ wallet: WalletCoreApiClient;
+ background: BackgroundApiClient;
+ listener: {
+ trigger: (d: ExtensionNotification) => void;
+ onUpdateNotification: typeof onUpdateNotification;
};
-}
+};
+
+function trigger(w: ExtensionNotification) {
+ platform.triggerWalletEvent({
+ type: "web-extension",
+ notification: w,
+ })
+}
+
+export const wxApi = {
+ wallet: new WalletApiClientImpl(),
+ background: new BackgroundApiClientImpl(),
+ listener: {
+ trigger,
+ onUpdateNotification,
+ },
+};
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index 4004f04f6..5fa255f5d 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -1,17 +1,17 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
@@ -23,28 +23,41 @@
/**
* Imports.
*/
-import { isFirefox, getPermissionsApi } from "./compat";
-import { extendedPermissions } from "./permissions";
import {
+ AbsoluteTime,
+ LogLevel,
+ Logger,
+ NotificationType,
OpenedPromise,
+ SetTimeoutTimerAPI,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TransactionMinorState,
+ WalletNotification,
+ getErrorDetailFromException,
+ makeErrorDetail,
openPromise,
- openTalerDatabase,
- makeErrorDetails,
- deleteTalerDatabase,
+ setGlobalLogLevelFromString,
+ setLogLevelFromString
+} from "@gnu-taler/taler-util";
+import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
+import {
DbAccess,
- WalletStoresV1,
+ SynchronousCryptoWorkerFactoryPlain,
Wallet,
+ WalletApiOperation,
+ WalletOperations,
+ WalletStoresV1,
+ deleteTalerDatabase,
+ exportDb,
+ importDb,
} from "@gnu-taler/taler-wallet-core";
-import {
- classifyTalerUri,
- CoreApiResponse,
- CoreApiResponseSuccess,
- TalerErrorCode,
- TalerUriType,
- WalletDiagnostics,
-} from "@gnu-taler/taler-util";
-import { BrowserHttpLib } from "./browserHttpLib";
-import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
+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 } from "./wxApi.js";
/**
* Currently active wallet instance. Might be unloaded and
@@ -56,357 +69,428 @@ let currentWallet: Wallet | undefined;
let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
-/**
- * Last version if an outdated DB, if applicable.
- */
-let outdatedDbVersion: number | undefined;
-
const walletInit: OpenedPromise<void> = openPromise<void>();
-const notificationPorts: chrome.runtime.Port[] = [];
+const logger = new Logger("wxBackend.ts");
-async function getDiagnostics(): Promise<WalletDiagnostics> {
- const manifestData = chrome.runtime.getManifest();
- const errors: string[] = [];
- let firefoxIdbProblem = false;
- let dbOutdated = false;
- try {
- await walletInit.promise;
- } catch (e) {
- errors.push("Error during wallet initialization: " + e);
- if (
- currentDatabase === undefined &&
- outdatedDbVersion === undefined &&
- isFirefox()
- ) {
- firefoxIdbProblem = true;
- }
- }
- if (!currentWallet) {
- errors.push("Could not create wallet backend.");
- }
- if (!currentDatabase) {
- errors.push("Could not open database");
- }
- if (outdatedDbVersion !== undefined) {
- errors.push(`Outdated DB version: ${outdatedDbVersion}`);
- dbOutdated = true;
- }
- const diagnostics: WalletDiagnostics = {
- walletManifestDisplayVersion: manifestData.version_name || "(undefined)",
- walletManifestVersion: manifestData.version,
- errors,
- firefoxIdbProblem,
- dbOutdated,
- };
- return diagnostics;
+type BackendHandlerType = {
+ [Op in keyof BackgroundOperations]: (
+ req: BackgroundOperations[Op]["request"],
+ ) => Promise<BackgroundOperations[Op]["response"]>;
+};
+
+type ExtensionHandlerType = {
+ [Op in keyof ExtensionOperations]: (
+ req: ExtensionOperations[Op]["request"],
+ ) => Promise<ExtensionOperations[Op]["response"]>;
+};
+
+async function resetDb(): Promise<void> {
+ await deleteTalerDatabase(indexedDB as any);
+ await reinitWallet();
}
-async function dispatch(
- req: any,
- sender: any,
- sendResponse: any,
-): Promise<void> {
- let r: CoreApiResponse;
-
- const wrapResponse = (result: unknown): CoreApiResponseSuccess => {
- return {
- type: "response",
- id: req.id,
- operation: req.operation,
- result,
- };
- };
+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++;
+}
- switch (req.operation) {
- case "wxGetDiagnostics": {
- r = wrapResponse(await getDiagnostics());
- break;
+//FIXME: maybe circular buffer
+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 "reset-db": {
- await deleteTalerDatabase(indexedDB);
- r = wrapResponse(await reinitWallet());
- break;
+ case NotificationType.BackupOperationError: {
+ const groupId = "";
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
}
- case "wxGetExtendedPermissions": {
- const res = await new Promise((resolve, reject) => {
- getPermissionsApi().contains(extendedPermissions, (result: boolean) => {
- resolve(result);
- });
+ 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,
});
- r = wrapResponse({ newValue: res });
- break;
+ return;
}
- case "wxSetExtendedPermissions": {
- const newVal = req.payload.value;
- console.log("new extended permissions value", newVal);
- if (newVal) {
- setupHeaderListener();
- r = wrapResponse({ newValue: true });
- } else {
- await new Promise<void>((resolve, reject) => {
- getPermissionsApi().remove(extendedPermissions, (rem) => {
- console.log("permissions removed:", rem);
- resolve();
- });
- });
- r = wrapResponse({ newVal: false });
+ 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;
}
- break;
+ list.push({
+ id: getUniqueId(),
+ type: n.type,
+ start,
+ end: AbsoluteTime.never(),
+ events: [ev],
+ groupId,
+ });
+ return;
}
- default: {
- const w = currentWallet;
- if (!w) {
- r = {
- type: "error",
- id: req.id,
- operation: req.operation,
- error: makeErrorDetails(
- TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
- "wallet core not available",
- {},
- ),
- };
- break;
+ 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;
}
- r = await w.handleCoreApiRequest(req.operation, req.id, req.payload);
- break;
+ 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;
}
}
+}
- try {
- sendResponse(r);
- } catch (e) {
- // might fail if tab disconnected
+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;
+ if (!dbBeforeGc) {
+ throw Error("no current db before running gc");
+ }
+ const dump = await exportDb(indexedDB as any);
+
+ await deleteTalerDatabase(indexedDB as any);
+ logger.info("cleaned");
+ await reinitWallet();
+ logger.info("init");
+
+ const dbAfterGc = currentDatabase;
+ if (!dbAfterGc) {
+ throw Error("no current db before running gc");
}
+ await importDb(dbAfterGc.idbHandle(), dump);
+ logger.info("imported");
}
-function getTab(tabId: number): Promise<chrome.tabs.Tab> {
- return new Promise((resolve, reject) => {
- chrome.tabs.get(tabId, (tab: chrome.tabs.Tab) => resolve(tab));
- });
+const extensionHandlers: ExtensionHandlerType = {
+ isAutoOpenEnabled,
+ isDomainTrusted,
+};
+
+async function isAutoOpenEnabled(): Promise<boolean> {
+ const settings = await platform.getSettingsFromStorage();
+ return settings.autoOpen === true;
}
-function setBadgeText(options: chrome.browserAction.BadgeTextDetails): void {
- // not supported by all browsers ...
- if (chrome && chrome.browserAction && chrome.browserAction.setBadgeText) {
- chrome.browserAction.setBadgeText(options);
+async function isDomainTrusted(): Promise<boolean> {
+ const settings = await platform.getSettingsFromStorage();
+ return settings.injectTalerSupport === true;
+}
+
+const backendHandlers: BackendHandlerType = {
+ resetDb,
+ runGarbageCollector,
+ getNotifications,
+ clearNotifications,
+ reinitWallet,
+ setLoggingLevel,
+};
+
+async function setLoggingLevel({
+ tag,
+ level,
+}: {
+ tag?: string;
+ level: LogLevel;
+}): Promise<void> {
+ logger.info(`setting ${tag} to ${level}`);
+ if (!tag) {
+ setGlobalLogLevelFromString(level);
} else {
- console.warn("can't set badge text, not supported", options);
+ setLogLevelFromString(tag, level);
}
}
+let nextMessageIndex = 0;
-function waitMs(timeoutMs: number): Promise<void> {
- return new Promise((resolve, reject) => {
- const bgPage = chrome.extension.getBackgroundPage();
- if (!bgPage) {
- reject("fatal: no background page");
- return;
+async function dispatch<
+ Op extends WalletOperations | BackgroundOperations | ExtensionOperations,
+>(req: MessageFromFrontend<Op> & { id: string }): Promise<MessageResponse> {
+ nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);
+
+ switch (req.channel) {
+ case "background": {
+ const handler = backendHandlers[req.operation] as (req: any) => any;
+ if (!handler) {
+ return {
+ type: "error",
+ id: req.id,
+ operation: String(req.operation),
+ error: getErrorDetailFromException(
+ Error(`unknown background operation`),
+ ),
+ };
+ }
+ try {
+ const result = await handler(req.payload);
+ return {
+ type: "response",
+ id: req.id,
+ operation: String(req.operation),
+ result,
+ };
+ } catch (er) {
+ return {
+ type: "error",
+ id: req.id,
+ error: getErrorDetailFromException(er),
+ operation: String(req.operation),
+ };
+ }
}
- bgPage.setTimeout(() => resolve(), timeoutMs);
- });
-}
+ case "extension": {
+ const handler = extensionHandlers[req.operation] as (req: any) => any;
+ if (!handler) {
+ return {
+ type: "error",
+ id: req.id,
+ operation: String(req.operation),
+ error: getErrorDetailFromException(
+ Error(`unknown extension operation`),
+ ),
+ };
+ }
+ try {
+ const result = await handler(req.payload);
+ return {
+ type: "response",
+ id: req.id,
+ operation: String(req.operation),
+ result,
+ };
+ } catch (er) {
+ return {
+ type: "error",
+ id: req.id,
+ error: getErrorDetailFromException(er),
+ operation: String(req.operation),
+ };
+ }
+ }
+ case "wallet": {
+ const w = currentWallet;
+ if (!w) {
+ const lastError: TalerErrorDetail =
+ walletInit.lastError instanceof TalerError
+ ? walletInit.lastError.errorDetail
+ : undefined;
-function makeSyncWalletRedirect(
- url: string,
- tabId: number,
- oldUrl: string,
- params?: { [name: string]: string | undefined },
-): Record<string, unknown> {
- const innerUrl = new URL(chrome.extension.getURL(url));
- if (params) {
- const hParams = Object.keys(params)
- .map((k) => `${k}=${params[k]}`)
- .join("&");
- innerUrl.hash = innerUrl.hash + "?" + hParams;
- }
- if (isFirefox()) {
- // Some platforms don't support the sync redirect (yet), so fall back to
- // async redirect after a timeout.
- const doit = async (): Promise<void> => {
- await waitMs(150);
- const tab = await getTab(tabId);
- if (tab.url === oldUrl) {
- chrome.tabs.update(tabId, { url: innerUrl.href });
+ return {
+ type: "error",
+ id: req.id,
+ operation: req.operation,
+ error: makeErrorDetail(
+ TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
+ { lastError },
+ `wallet core not available${
+ !lastError ? "" : `,last error: ${lastError.hint}`
+ }`,
+ ),
+ };
}
- };
- doit();
+ //multiple client can create the same id, send the wallet an unique key
+ const newId = `${req.id}_${nextMessageIndex}`;
+ const resp = await w.handleCoreApiRequest(
+ req.operation,
+ newId,
+ req.payload,
+ );
+ //return to the client the original id
+ resp.id = req.id;
+ return resp;
+ }
}
- console.log("redirecting to", innerUrl.href);
- chrome.tabs.update(tabId, { url: innerUrl.href });
- return { redirectUrl: innerUrl.href };
+
+ const anyReq = req as any;
+ return {
+ type: "error",
+ id: anyReq.id,
+ operation: String(anyReq.operation),
+ error: getErrorDetailFromException(
+ Error(
+ `unknown channel ${anyReq.channel}, should be "background", "extension" or "wallet"`,
+ ),
+ ),
+ };
}
async function reinitWallet(): Promise<void> {
if (currentWallet) {
- currentWallet.stop();
+ await currentWallet.client.call(WalletApiOperation.Shutdown, {});
currentWallet = undefined;
}
currentDatabase = undefined;
- setBadgeText({ text: "" });
- try {
- currentDatabase = await openTalerDatabase(indexedDB, reinitWallet);
- } catch (e) {
- console.error("could not open database", e);
- walletInit.reject(e);
- return;
+ // setBadgeText({ text: "" });
+ let cryptoWorker;
+ let timer;
+
+ const httpFactory = (): HttpRequestLibrary => {
+ return new BrowserFetchHttpLib({
+ // enableThrottling: false,
+ });
+ };
+
+ if (platform.useServiceWorkerAsBackgroundProcess()) {
+ cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
+ timer = new SetTimeoutTimerAPI();
+ } else {
+ // We could (should?) use the BrowserCryptoWorkerFactory here,
+ // but right now we don't, to have less platform differences.
+ // cryptoWorker = new BrowserCryptoWorkerFactory();
+ cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
+ timer = new SetTimeoutTimerAPI();
}
- const http = new BrowserHttpLib();
- console.log("setting wallet");
+
+ const settings = await platform.getSettingsFromStorage();
+ logger.info("Setting up wallet");
const wallet = await Wallet.create(
- currentDatabase,
- http,
- new BrowserCryptoWorkerFactory(),
+ indexedDB as any,
+ httpFactory as any,
+ timer,
+ cryptoWorker,
);
try {
- await wallet.handleCoreApiRequest("initWallet", "native-init", {});
+ await wallet.handleCoreApiRequest("initWallet", "native-init", {
+ config: {
+ testing: {
+ emitObservabilityEvents: settings.showWalletActivity,
+ devModeActive: settings.advancedMode,
+ },
+ features: {
+ allowHttp: settings.walletAllowHttp,
+ },
+ },
+ });
} catch (e) {
- console.error("could not initialize wallet", e);
+ logger.error("could not initialize wallet", e);
walletInit.reject(e);
return;
}
- wallet.addNotificationListener((x) => {
- for (const x of notificationPorts) {
- try {
- x.postMessage({ type: "notification" });
- } catch (e) {
- console.error(e);
- }
+ wallet.addNotificationListener((message) => {
+ if (settings.showWalletActivity) {
+ addNewWalletActivityNotification(activity, message);
}
- });
- wallet.runTaskLoop().catch((e) => {
- console.log("error during wallet task loop", e);
- });
- // Useful for debugging in the background page.
- (window as any).talerWallet = wallet;
- currentWallet = wallet;
- walletInit.resolve();
-}
-try {
- // This needs to be outside of main, as Firefox won't fire the event if
- // the listener isn't created synchronously on loading the backend.
- chrome.runtime.onInstalled.addListener((details) => {
- console.log("onInstalled with reason", details.reason);
- if (details.reason === "install") {
- const url = chrome.extension.getURL("/static/wallet.html#/welcome");
- chrome.tabs.create({ active: true, url: url });
- }
+ processWalletNotification(message);
+
+ platform.sendMessageToAllChannels({
+ type: "wallet",
+ notification: message,
+ });
});
-} catch (e) {
- console.error(e);
-}
-function headerListener(
- details: chrome.webRequest.WebResponseHeadersDetails,
-): chrome.webRequest.BlockingResponse | undefined {
- console.log("header listener");
- if (chrome.runtime.lastError) {
- console.error(chrome.runtime.lastError);
- return;
- }
- const wallet = currentWallet;
- if (!wallet) {
- console.warn("wallet not available while handling header");
- return;
- }
- console.log("in header listener");
- if (
- details.statusCode === 402 ||
- details.statusCode === 202 ||
- details.statusCode === 200
- ) {
- console.log(`got 402/202 from ${details.url}`);
- for (const header of details.responseHeaders || []) {
- if (header.name.toLowerCase() === "taler") {
- const talerUri = header.value || "";
- const uriType = classifyTalerUri(talerUri);
- switch (uriType) {
- case TalerUriType.TalerWithdraw:
- return makeSyncWalletRedirect(
- "/static/wallet.html#/withdraw",
- details.tabId,
- details.url,
- {
- talerWithdrawUri: talerUri,
- },
- );
- case TalerUriType.TalerPay:
- return makeSyncWalletRedirect(
- "/static/wallet.html#/pay",
- details.tabId,
- details.url,
- {
- talerPayUri: talerUri,
- },
- );
- case TalerUriType.TalerTip:
- return makeSyncWalletRedirect(
- "/static/wallet.html#/tip",
- details.tabId,
- details.url,
- {
- talerTipUri: talerUri,
- },
- );
- case TalerUriType.TalerRefund:
- return makeSyncWalletRedirect(
- "/static/wallet.html#/refund",
- details.tabId,
- details.url,
- {
- talerRefundUri: talerUri,
- },
- );
- case TalerUriType.TalerNotifyReserve:
- Promise.resolve().then(() => {
- const w = currentWallet;
- if (!w) {
- return;
- }
- // FIXME: Is this still useful?
- // handleNotifyReserve(w);
- });
- break;
- default:
- console.warn(
- "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
- );
- break;
- }
- }
- }
+ // Useful for debugging in the background page.
+ if (typeof window !== "undefined") {
+ (window as any).talerWallet = wallet;
}
- return;
-}
-
-function setupHeaderListener(): void {
- console.log("setting up header listener");
- // Handlers for catching HTTP requests
- getPermissionsApi().contains(extendedPermissions, (result: boolean) => {
- if (
- "webRequest" in chrome &&
- "onHeadersReceived" in chrome.webRequest &&
- chrome.webRequest.onHeadersReceived.hasListener(headerListener)
- ) {
- chrome.webRequest.onHeadersReceived.removeListener(headerListener);
- }
- if (result) {
- console.log("actually adding listener");
- chrome.webRequest.onHeadersReceived.addListener(
- headerListener,
- { urls: ["<all_urls>"] },
- ["responseHeaders", "blocking"],
- );
- }
- if ("webRequest" in chrome) {
- chrome.webRequest.handlerBehaviorChanged(() => {
- if (chrome.runtime.lastError) {
- console.error(chrome.runtime.lastError);
- }
- });
- }
- });
+ currentWallet = wallet;
+ updateIconBasedOnBalance();
+ return walletInit.resolve();
}
/**
@@ -415,44 +499,74 @@ function setupHeaderListener(): void {
* Sets up all event handlers and other machinery.
*/
export async function wxMain(): Promise<void> {
- // Explicitly unload the extension page as soon as an update is available,
- // so the update gets installed as soon as possible.
- chrome.runtime.onUpdateAvailable.addListener((details) => {
- console.log("update available:", details);
- chrome.runtime.reload();
- });
- reinitWallet();
+ logger.trace("starting");
+ const afterWalletIsInitialized = reinitWallet();
+
+ logger.trace("reload on new version");
+ platform.registerReloadOnNewVersion();
// Handlers for messages coming directly from the content
// script on the page
- chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
- dispatch(req, sender, sendResponse);
- return true;
+ logger.trace("listen all channels");
+ platform.listenToAllChannels(async (message) => {
+ //wait until wallet is initialized
+ await afterWalletIsInitialized;
+ const result = await dispatch(message);
+ return result;
});
- chrome.runtime.onConnect.addListener((port) => {
- notificationPorts.push(port);
- port.onDisconnect.addListener((discoPort) => {
- const idx = notificationPorts.indexOf(discoPort);
- if (idx >= 0) {
- notificationPorts.splice(idx, 1);
- }
- });
- });
+ logger.trace("register all incoming connections");
+ platform.registerAllIncomingConnections();
+ logger.trace("redirect if first start");
try {
- setupHeaderListener();
+ platform.registerOnInstalled(() => {
+ platform.openWalletPage("/welcome");
+ });
} catch (e) {
- console.log(e);
+ console.error(e);
}
+}
- // On platforms that support it, also listen to external
- // modification of permissions.
- getPermissionsApi().addPermissionsListener((perm) => {
- if (chrome.runtime.lastError) {
- console.error(chrome.runtime.lastError);
- return;
+async function updateIconBasedOnBalance() {
+ const balance = await currentWallet?.client.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ if (balance) {
+ let showAlert = false;
+ for (const b of balance.balances) {
+ if (b.flags.length > 0) {
+ console.log("b.flags", JSON.stringify(b.flags));
+ showAlert = true;
+ break;
+ }
}
- setupHeaderListener();
- });
+
+ 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();
+ }
}