aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json3
-rw-r--r--packages/anastasis-core/src/anastasis-data.ts17
-rw-r--r--packages/anastasis-core/src/crypto.ts2
-rw-r--r--packages/anastasis-core/src/index.ts124
-rw-r--r--packages/anastasis-core/src/policy-suggestion.test.ts44
-rw-r--r--packages/anastasis-core/src/policy-suggestion.ts25
-rw-r--r--packages/anastasis-core/src/reducer-types.ts13
-rw-r--r--packages/anastasis-webui/package.json12
-rw-r--r--packages/anastasis-webui/preact.config.js45
-rw-r--r--packages/anastasis-webui/src/.babelrc4
-rw-r--r--packages/anastasis-webui/src/components/AsyncButton.tsx37
-rw-r--r--packages/anastasis-webui/src/components/Notifications.tsx59
-rw-r--r--packages/anastasis-webui/src/components/QR.tsx27
-rw-r--r--packages/anastasis-webui/src/components/app.tsx1
-rw-r--r--packages/anastasis-webui/src/components/fields/DateInput.tsx112
-rw-r--r--packages/anastasis-webui/src/components/fields/EmailInput.tsx55
-rw-r--r--packages/anastasis-webui/src/components/fields/FileInput.tsx125
-rw-r--r--packages/anastasis-webui/src/components/fields/ImageInput.tsx96
-rw-r--r--packages/anastasis-webui/src/components/fields/NumberInput.tsx55
-rw-r--r--packages/anastasis-webui/src/components/fields/TextInput.tsx53
-rw-r--r--packages/anastasis-webui/src/components/menu/LangSelector.tsx99
-rw-r--r--packages/anastasis-webui/src/components/menu/NavigationBar.tsx81
-rw-r--r--packages/anastasis-webui/src/components/menu/SideBar.tsx304
-rw-r--r--packages/anastasis-webui/src/components/menu/index.tsx133
-rw-r--r--packages/anastasis-webui/src/components/picker/DatePicker.tsx314
-rw-r--r--packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx45
-rw-r--r--packages/anastasis-webui/src/components/picker/DurationPicker.tsx217
-rw-r--r--packages/anastasis-webui/src/context/anastasis.ts20
-rw-r--r--packages/anastasis-webui/src/context/translation.ts43
-rw-r--r--packages/anastasis-webui/src/declaration.d.ts28
-rw-r--r--packages/anastasis-webui/src/hooks/async.ts29
-rw-r--r--packages/anastasis-webui/src/hooks/index.ts113
-rw-r--r--packages/anastasis-webui/src/i18n/index.tsx54
-rw-r--r--packages/anastasis-webui/src/i18n/strings.ts38
-rw-r--r--packages/anastasis-webui/src/index.ts4
-rw-r--r--packages/anastasis-webui/src/manifest.json2
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx53
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx289
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx123
-rw-r--r--packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx146
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx113
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx303
-rw-r--r--packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx48
-rw-r--r--packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx70
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx114
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx192
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx22
-rw-r--r--packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx18
-rw-r--r--packages/anastasis-webui/src/pages/home/ConfirmModal.tsx58
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx33
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx115
-rw-r--r--packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx190
-rw-r--r--packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx200
-rw-r--r--packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx44
-rw-r--r--packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx17
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx36
-rw-r--r--packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx89
-rw-r--r--packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx217
-rw-r--r--packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx158
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx22
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx84
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx24
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx213
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx117
-rw-r--r--packages/anastasis-webui/src/pages/home/SolveScreen.tsx315
-rw-r--r--packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx22
-rw-r--r--packages/anastasis-webui/src/pages/home/StartScreen.tsx26
-rw-r--r--packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx26
-rw-r--r--packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx16
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx85
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx107
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx90
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx148
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx85
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx118
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.tsx112
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx84
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx116
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx117
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx86
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx105
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx258
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.tsx121
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx84
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx83
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx148
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx84
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx115
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.tsx118
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx85
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx88
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.stories.tsx60
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.tsx112
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/index.tsx94
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/totp.ts57
-rw-r--r--packages/anastasis-webui/src/pages/home/index.tsx103
-rw-r--r--packages/anastasis-webui/src/pages/notfound/index.tsx22
-rw-r--r--packages/anastasis-webui/src/pages/profile/index.tsx61
-rw-r--r--packages/anastasis-webui/src/scss/DurationPicker.scss1
-rw-r--r--packages/anastasis-webui/src/scss/_aside.scss100
-rw-r--r--packages/anastasis-webui/src/scss/_card.scss4
-rw-r--r--packages/anastasis-webui/src/scss/_custom-calendar.scss83
-rw-r--r--packages/anastasis-webui/src/scss/_footer.scss2
-rw-r--r--packages/anastasis-webui/src/scss/_form.scss33
-rw-r--r--packages/anastasis-webui/src/scss/_hero-bar.scss8
-rw-r--r--packages/anastasis-webui/src/scss/_main-section.scss2
-rw-r--r--packages/anastasis-webui/src/scss/_mixins.scss6
-rw-r--r--packages/anastasis-webui/src/scss/_modal.scss2
-rw-r--r--packages/anastasis-webui/src/scss/_nav-bar.scss16
-rw-r--r--packages/anastasis-webui/src/scss/_table.scss28
-rw-r--r--packages/anastasis-webui/src/scss/_tiles.scss3
-rw-r--r--packages/anastasis-webui/src/scss/_title-bar.scss8
-rw-r--r--packages/anastasis-webui/src/scss/main.scss9
-rw-r--r--packages/anastasis-webui/src/template.html62
-rw-r--r--packages/anastasis-webui/src/utils/index.tsx159
-rw-r--r--packages/taler-util/src/backupTypes.ts5
-rw-r--r--packages/taler-util/src/helpers.ts11
-rw-r--r--packages/taler-util/src/payto.ts45
-rw-r--r--packages/taler-util/src/talerCrypto.ts19
-rw-r--r--packages/taler-util/src/talerTypes.ts77
-rw-r--r--packages/taler-util/src/taleruri.ts7
-rw-r--r--packages/taler-util/src/walletTypes.ts8
-rw-r--r--packages/taler-wallet-cli/src/harness/harness.ts279
-rw-r--r--packages/taler-wallet-cli/src/harness/helpers.ts26
-rw-r--r--packages/taler-wallet-cli/src/harness/libeufin-apis.ts8
-rw-r--r--packages/taler-wallet-cli/src/harness/libeufin.ts8
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts7
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-deposit.ts4
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts7
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts7
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts5
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts7
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts5
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts5
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts5
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts16
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts7
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-revocation.ts7
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts7
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-tipping.ts4
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts5
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts8
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts10
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts1
-rw-r--r--packages/taler-wallet-core/package.json2
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoTypes.ts6
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts2
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts124
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoWorkerInterface.ts (renamed from packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts)0
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts4
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts114
-rw-r--r--packages/taler-wallet-core/src/db.ts13
-rw-r--r--packages/taler-wallet-core/src/errors.ts2
-rw-r--r--packages/taler-wallet-core/src/headless/helpers.ts26
-rw-r--r--packages/taler-wallet-core/src/index.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts9
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts19
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts9
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts42
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts5
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts25
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts15
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.test.ts44
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts35
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.test.ts5
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts21
-rw-r--r--packages/taler-wallet-core/src/wallet.ts3
-rw-r--r--packages/taler-wallet-webextension/package.json3
-rw-r--r--packages/taler-wallet-webextension/src/NavigationBar.tsx79
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js35
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts5
-rw-r--r--packages/taler-wallet-webextension/src/browserWorkerEntry.ts2
-rw-r--r--packages/taler-wallet-webextension/src/compat.js57
-rw-r--r--packages/taler-wallet-webextension/src/components/Checkbox.tsx36
-rw-r--r--packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx48
-rw-r--r--packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx13
-rw-r--r--packages/taler-wallet-webextension/src/components/Diagnostics.tsx75
-rw-r--r--packages/taler-wallet-webextension/src/components/EditableText.tsx73
-rw-r--r--packages/taler-wallet-webextension/src/components/ErrorMessage.tsx41
-rw-r--r--packages/taler-wallet-webextension/src/components/ExchangeToS.tsx88
-rw-r--r--packages/taler-wallet-webextension/src/components/LogoHeader.tsx31
-rw-r--r--packages/taler-wallet-webextension/src/components/Part.tsx28
-rw-r--r--packages/taler-wallet-webextension/src/components/QR.tsx53
-rw-r--r--packages/taler-wallet-webextension/src/components/SelectList.tsx97
-rw-r--r--packages/taler-wallet-webextension/src/components/Time.tsx41
-rw-r--r--packages/taler-wallet-webextension/src/components/TransactionItem.tsx83
-rw-r--r--packages/taler-wallet-webextension/src/components/styled/index.tsx290
-rw-r--r--packages/taler-wallet-webextension/src/context/devContext.ts24
-rw-r--r--packages/taler-wallet-webextension/src/context/translation.ts56
-rw-r--r--packages/taler-wallet-webextension/src/cta/Pay.stories.tsx190
-rw-r--r--packages/taler-wallet-webextension/src/cta/Pay.tsx361
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund.stories.tsx72
-rw-r--r--packages/taler-wallet-webextension/src/cta/Refund.tsx69
-rw-r--r--packages/taler-wallet-webextension/src/cta/Tip.stories.tsx48
-rw-r--r--packages/taler-wallet-webextension/src/cta/Tip.tsx55
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx561
-rw-r--r--packages/taler-wallet-webextension/src/cta/Withdraw.tsx501
-rw-r--r--packages/taler-wallet-webextension/src/cta/payback.tsx5
-rw-r--r--packages/taler-wallet-webextension/src/cta/reset-required.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/cta/return-coins.tsx5
-rw-r--r--packages/taler-wallet-webextension/src/custom.d.ts2
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts4
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts25
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts50
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useBalances.ts3
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts6
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts9
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useLang.ts13
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts44
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts4
-rw-r--r--packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts9
-rw-r--r--packages/taler-wallet-webextension/src/i18n/strings.ts2
-rw-r--r--packages/taler-wallet-webextension/src/popup/Backup.stories.tsx307
-rw-r--r--packages/taler-wallet-webextension/src/popup/BackupPage.tsx181
-rw-r--r--packages/taler-wallet-webextension/src/popup/Balance.stories.tsx253
-rw-r--r--packages/taler-wallet-webextension/src/popup/BalancePage.tsx230
-rw-r--r--packages/taler-wallet-webextension/src/popup/Debug.tsx15
-rw-r--r--packages/taler-wallet-webextension/src/popup/History.stories.tsx184
-rw-r--r--packages/taler-wallet-webextension/src/popup/History.tsx126
-rw-r--r--packages/taler-wallet-webextension/src/popup/Popup.stories.tsx29
-rw-r--r--packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx41
-rw-r--r--packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx40
-rw-r--r--packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx323
-rw-r--r--packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx269
-rw-r--r--packages/taler-wallet-webextension/src/popup/Settings.stories.tsx19
-rw-r--r--packages/taler-wallet-webextension/src/popup/Settings.tsx93
-rw-r--r--packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx24
-rw-r--r--packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx161
-rw-r--r--packages/taler-wallet-webextension/src/popupEntryPoint.tsx77
-rw-r--r--packages/taler-wallet-webextension/src/renderHtml.tsx51
-rw-r--r--packages/taler-wallet-webextension/src/test-utils.ts16
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx307
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BackupPage.tsx181
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx84
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BalancePage.tsx139
-rw-r--r--packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx42
-rw-r--r--packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx151
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.stories.tsx198
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.tsx122
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx109
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx41
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx40
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx323
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx312
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx43
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx161
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx32
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Settings.tsx109
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx213
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx432
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx20
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Welcome.tsx79
-rw-r--r--packages/taler-wallet-webextension/src/walletEntryPoint.tsx128
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts104
-rw-r--r--packages/taler-wallet-webextension/static/wallet.html20
-rw-r--r--packages/taler-wallet-webextension/tsconfig.json20
-rw-r--r--pnpm-lock.yaml416
262 files changed, 13148 insertions, 7219 deletions
diff --git a/package.json b/package.json
index b6e27b997..e00de0c53 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"devDependencies": {
"@linaria/esbuild": "^3.0.0-beta.13",
"@linaria/shaker": "^3.0.0-beta.13",
- "esbuild": "^0.12.29"
+ "esbuild": "^0.12.29",
+ "prettier": "^2.4.1"
}
}
diff --git a/packages/anastasis-core/src/anastasis-data.ts b/packages/anastasis-core/src/anastasis-data.ts
index 4946e9dfd..c67883a2e 100644
--- a/packages/anastasis-core/src/anastasis-data.ts
+++ b/packages/anastasis-core/src/anastasis-data.ts
@@ -1,6 +1,7 @@
// This file is auto-generated, do not modify.
// Generated from v0.2.0-4-g61ea83c on Tue, 05 Oct 2021 10:40:32 +0200
// To re-generate, run contrib/gen-ts.sh from the main anastasis code base.
+// XXX: Modified for demo, allowing demo providers for EUR
export const anastasisData = {
providersList: {
@@ -16,6 +17,22 @@ export const anastasisData = {
currency: "KUDOS",
},
{
+ url: "https://anastasis.demo.taler.net/",
+ currency: "EUR",
+ },
+ {
+ url: "https://kudos.demo.anastasis.lu/",
+ currency: "EUR",
+ },
+ {
+ url: "https://anastasis.demo.taler.net/",
+ currency: "CHF",
+ },
+ {
+ url: "https://kudos.demo.anastasis.lu/",
+ currency: "CHF",
+ },
+ {
url: "http://localhost:8086/",
currency: "TESTKUDOS",
},
diff --git a/packages/anastasis-core/src/crypto.ts b/packages/anastasis-core/src/crypto.ts
index 206d9eca8..75bd4b323 100644
--- a/packages/anastasis-core/src/crypto.ts
+++ b/packages/anastasis-core/src/crypto.ts
@@ -11,8 +11,6 @@ import {
stringToBytes,
secretbox_open,
hash,
- Logger,
- j2s,
} from "@gnu-taler/taler-util";
import { argon2id } from "hash-wasm";
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts
index 362ac3317..15e1e5d97 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -65,6 +65,8 @@ import {
ActionArgsChangeVersion,
TruthMetaData,
ActionArgsUpdatePolicy,
+ ActionArgsAddProvider,
+ ActionArgsDeleteProvider,
} from "./reducer-types.js";
import fetchPonyfill from "fetch-ponyfill";
import {
@@ -109,13 +111,23 @@ export * from "./challenge-feedback-types.js";
const logger = new Logger("anastasis-core:index.ts");
-function getContinents(): ContinentInfo[] {
+function getContinents(
+ opts: { requireProvider?: boolean } = {},
+): ContinentInfo[] {
+ const currenciesWithProvider = new Set<string>();
+ anastasisData.providersList.anastasis_provider.forEach((x) => {
+ currenciesWithProvider.add(x.currency);
+ });
const continentSet = new Set<string>();
const continents: ContinentInfo[] = [];
for (const country of anastasisData.countriesList.countries) {
if (continentSet.has(country.continent)) {
continue;
}
+ if (opts.requireProvider && !currenciesWithProvider.has(country.currency)) {
+ // Country's currency doesn't have any providers => skip
+ continue;
+ }
continentSet.add(country.continent);
continents.push({
...{ name_i18n: country.continent_i18n },
@@ -148,9 +160,18 @@ export class ReducerError extends Error {
* Get countries for a continent, abort with ReducerError
* exception when continent doesn't exist.
*/
-function getCountries(continent: string): CountryInfo[] {
+function getCountries(
+ continent: string,
+ opts: { requireProvider?: boolean } = {},
+): CountryInfo[] {
+ const currenciesWithProvider = new Set<string>();
+ anastasisData.providersList.anastasis_provider.forEach((x) => {
+ currenciesWithProvider.add(x.currency);
+ });
const countries = anastasisData.countriesList.countries.filter(
- (x) => x.continent === continent,
+ (x) =>
+ x.continent === continent &&
+ (!opts.requireProvider || currenciesWithProvider.has(x.currency)),
);
if (countries.length <= 0) {
throw new ReducerError({
@@ -164,14 +185,18 @@ function getCountries(continent: string): CountryInfo[] {
export async function getBackupStartState(): Promise<ReducerStateBackup> {
return {
backup_state: BackupStates.ContinentSelecting,
- continents: getContinents(),
+ continents: getContinents({
+ requireProvider: true,
+ }),
};
}
export async function getRecoveryStartState(): Promise<ReducerStateRecovery> {
return {
recovery_state: RecoveryStates.ContinentSelecting,
- continents: getContinents(),
+ continents: getContinents({
+ requireProvider: true,
+ }),
};
}
@@ -952,6 +977,21 @@ async function requestTruth(
}
if (resp.status === HttpStatusCode.Forbidden) {
+ const body = await resp.json();
+ if (
+ body.code === TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED
+ ) {
+ return {
+ ...state,
+ recovery_state: RecoveryStates.ChallengeSolving,
+ challenge_feedback: {
+ ...state.challenge_feedback,
+ [truth.uuid]: {
+ state: ChallengeFeedbackStatus.Pending,
+ },
+ },
+ };
+ }
return {
...state,
recovery_state: RecoveryStates.ChallengeSolving,
@@ -959,7 +999,7 @@ async function requestTruth(
...state.challenge_feedback,
[truth.uuid]: {
state: ChallengeFeedbackStatus.Message,
- message: "Challenge should be solved",
+ message: body.hint ?? "Challenge should be solved",
},
},
};
@@ -1022,9 +1062,15 @@ async function recoveryEnterUserAttributes(
args: ActionArgsEnterUserAttributes,
): Promise<ReducerStateRecovery | ReducerStateError> {
// FIXME: validate attributes
+ const providerUrls = Object.keys(state.authentication_providers ?? {});
+ const newProviders = state.authentication_providers ?? {};
+ for (const url of providerUrls) {
+ newProviders[url] = await getProviderInfo(url);
+ }
const st: ReducerStateRecovery = {
...state,
identity_attributes: args.identity_attributes,
+ authentication_providers: newProviders,
};
return downloadPolicy(st);
}
@@ -1058,7 +1104,9 @@ async function backupSelectContinent(
state: ReducerStateBackup,
args: ActionArgsSelectContinent,
): Promise<ReducerStateBackup | ReducerStateError> {
- const countries = getCountries(args.continent);
+ const countries = getCountries(args.continent, {
+ requireProvider: true,
+ });
if (countries.length <= 0) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_INPUT_INVALID,
@@ -1077,7 +1125,9 @@ async function recoverySelectContinent(
state: ReducerStateRecovery,
args: ActionArgsSelectContinent,
): Promise<ReducerStateRecovery | ReducerStateError> {
- const countries = getCountries(args.continent);
+ const countries = getCountries(args.continent, {
+ requireProvider: true,
+ });
return {
...state,
recovery_state: RecoveryStates.CountrySelecting,
@@ -1132,6 +1182,60 @@ function transitionRecoveryJump(
};
}
+//FIXME: doest the same that addProviderRecovery, but type are not generic enough
+async function addProviderBackup(
+ state: ReducerStateBackup,
+ args: ActionArgsAddProvider,
+): Promise<ReducerStateBackup> {
+ const info = await getProviderInfo(args.provider_url)
+ return {
+ ...state,
+ authentication_providers: {
+ ...(state.authentication_providers ?? {}),
+ [args.provider_url]: info,
+ },
+ };
+}
+
+//FIXME: doest the same that deleteProviderRecovery, but type are not generic enough
+async function deleteProviderBackup(
+ state: ReducerStateBackup,
+ args: ActionArgsDeleteProvider,
+): Promise<ReducerStateBackup> {
+ const authentication_providers = {... state.authentication_providers ?? {} }
+ delete authentication_providers[args.provider_url]
+ return {
+ ...state,
+ authentication_providers,
+ };
+}
+
+async function addProviderRecovery(
+ state: ReducerStateRecovery,
+ args: ActionArgsAddProvider,
+): Promise<ReducerStateRecovery> {
+ const info = await getProviderInfo(args.provider_url)
+ return {
+ ...state,
+ authentication_providers: {
+ ...(state.authentication_providers ?? {}),
+ [args.provider_url]: info,
+ },
+ };
+}
+
+async function deleteProviderRecovery(
+ state: ReducerStateRecovery,
+ args: ActionArgsDeleteProvider,
+): Promise<ReducerStateRecovery> {
+ const authentication_providers = {... state.authentication_providers ?? {} }
+ delete authentication_providers[args.provider_url]
+ return {
+ ...state,
+ authentication_providers,
+ };
+}
+
async function addAuthentication(
state: ReducerStateBackup,
args: ActionArgsAddAuthentication,
@@ -1366,6 +1470,8 @@ const backupTransitions: Record<
...transitionBackupJump("back", BackupStates.UserAttributesCollecting),
...transition("add_authentication", codecForAny(), addAuthentication),
...transition("delete_authentication", codecForAny(), deleteAuthentication),
+ ...transition("add_provider", codecForAny(), addProviderBackup),
+ ...transition("delete_provider", codecForAny(), deleteProviderBackup),
...transition("next", codecForAny(), nextFromAuthenticationsEditing),
},
[BackupStates.PoliciesReviewing]: {
@@ -1434,6 +1540,8 @@ const recoveryTransitions: Record<
[RecoveryStates.SecretSelecting]: {
...transitionRecoveryJump("back", RecoveryStates.UserAttributesCollecting),
...transitionRecoveryJump("next", RecoveryStates.ChallengeSelecting),
+ ...transition("add_provider", codecForAny(), addProviderRecovery),
+ ...transition("delete_provider", codecForAny(), deleteProviderRecovery),
...transition(
"change_version",
codecForActionArgsChangeVersion(),
diff --git a/packages/anastasis-core/src/policy-suggestion.test.ts b/packages/anastasis-core/src/policy-suggestion.test.ts
new file mode 100644
index 000000000..6370825da
--- /dev/null
+++ b/packages/anastasis-core/src/policy-suggestion.test.ts
@@ -0,0 +1,44 @@
+import { j2s } from "@gnu-taler/taler-util";
+import test from "ava";
+import { ProviderInfo, suggestPolicies } from "./policy-suggestion.js";
+
+test("policy suggestion", async (t) => {
+ const methods = [
+ {
+ challenge: "XXX",
+ instructions: "SMS to 123",
+ type: "sms",
+ },
+ {
+ challenge: "XXX",
+ instructions: "What is the meaning of life?",
+ type: "question",
+ },
+ {
+ challenge: "XXX",
+ instructions: "email to foo@bar.com",
+ type: "email",
+ },
+ ];
+ const providers: ProviderInfo[] = [
+ {
+ methodCost: {
+ sms: "KUDOS:1",
+ },
+ url: "prov1",
+ },
+ {
+ methodCost: {
+ question: "KUDOS:1",
+ },
+ url: "prov2",
+ },
+ ];
+ const res1 = suggestPolicies(methods, providers);
+ t.assert(res1.policies.length === 1);
+ const res2 = suggestPolicies([...methods].reverse(), providers);
+ t.assert(res2.policies.length === 1);
+
+ const res3 = suggestPolicies(methods, [...providers].reverse());
+ t.assert(res3.policies.length === 1);
+});
diff --git a/packages/anastasis-core/src/policy-suggestion.ts b/packages/anastasis-core/src/policy-suggestion.ts
index 7eb6c21cc..2c25caaa4 100644
--- a/packages/anastasis-core/src/policy-suggestion.ts
+++ b/packages/anastasis-core/src/policy-suggestion.ts
@@ -84,9 +84,16 @@ function assignProviders(
for (const provSel of providerSelections) {
// First, check if selection is even possible with the methods offered
let possible = true;
- for (const methIndex in provSel) {
- const provIndex = provSel[methIndex];
+ for (const methSelIndex in provSel) {
+ const provIndex = provSel[methSelIndex];
+ if (typeof provIndex !== "number") {
+ throw Error("invariant failed");
+ }
+ const methIndex = methodSelection[methSelIndex];
const meth = methods[methIndex];
+ if (!meth) {
+ throw Error("invariant failed");
+ }
const prov = providers[provIndex];
if (!prov.methodCost[meth.type]) {
possible = false;
@@ -96,7 +103,6 @@ function assignProviders(
if (!possible) {
continue;
}
-
// Evaluate diversity, always prefer policies
// that increase diversity.
const providerSet = new Set<string>();
@@ -163,10 +169,19 @@ function assignProviders(
/**
* A provider selection maps a method selection index to a provider index.
+ *
+ * I.e. "PSEL[i] = x" means that provider with index "x" should be used
+ * for method with index "MSEL[i]"
*/
type ProviderSelection = number[];
/**
+ * A method selection "MSEL[j] = y" means that policy method j
+ * should use method y.
+ */
+type MethodSelection = number[];
+
+/**
* Compute provider mappings.
* Enumerates all n-combinations with repetition of m providers.
*/
@@ -184,7 +199,7 @@ function enumerateProviderMappings(
}
for (let j = start; j < m; j++) {
a[i] = j;
- sel(i + 1, j);
+ sel(i + 1, 0);
if (limit && selections.length >= limit) {
break;
}
@@ -199,8 +214,6 @@ interface PolicySelectionResult {
policy_providers: PolicyProvider[];
}
-type MethodSelection = number[];
-
/**
* Compute method selections.
* Enumerates all n-combinations without repetition of m methods.
diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts
index 0f64be4eb..3e6d6c852 100644
--- a/packages/anastasis-core/src/reducer-types.ts
+++ b/packages/anastasis-core/src/reducer-types.ts
@@ -50,6 +50,11 @@ export interface SuccessDetails {
export interface CoreSecret {
mime: string;
value: string;
+ /**
+ * Filename, only set if the secret comes from
+ * a file. Should be set unless the mime type is "text/plain";
+ */
+ filename?: string;
}
export interface ReducerStateBackup {
@@ -329,6 +334,14 @@ export const codecForActionArgsEnterUserAttributes = () =>
.property("identity_attributes", codecForAny())
.build("ActionArgsEnterUserAttributes");
+export interface ActionArgsAddProvider {
+ provider_url: string;
+}
+
+export interface ActionArgsDeleteProvider {
+ provider_url: string;
+}
+
export interface ActionArgsAddAuthentication {
authentication_method: {
type: string;
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
index 96d2d65f9..d35b6ba27 100644
--- a/packages/anastasis-webui/package.json
+++ b/packages/anastasis-webui/package.json
@@ -4,12 +4,15 @@
"version": "0.0.0",
"license": "MIT",
"scripts": {
- "build": "preact build --no-sw --no-esm",
- "serve": "sirv build --port 8080 --cors --single",
- "dev": "preact watch --no-sw --no-esm",
+ "build": "preact build --no-sw --no-esm --no-inline-css",
+ "serve": "sirv build --port ${PORT:=8080} --cors --single",
+ "dev": "preact watch --port ${PORT:=8080} --no-sw --no-esm",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"test": "jest ./tests",
"build-storybook": "build-storybook",
+ "build-single": "preact build --no-sw --no-esm -c preact.single-config.js --dest single && sh remove-link-stylesheet.sh",
+ "serve-single": "sirv single --port ${PORT:=8080} --cors --single",
+ "pretty": "prettier --write src",
"storybook": "start-storybook -p 6006"
},
"eslintConfig": {
@@ -25,6 +28,7 @@
"dependencies": {
"@gnu-taler/taler-util": "workspace:^0.8.3",
"anastasis-core": "workspace:^0.0.1",
+ "base64-inline-loader": "1.1.1",
"date-fns": "2.25.0",
"jed": "1.1.1",
"preact": "^10.5.15",
@@ -67,4 +71,4 @@
"<rootDir>/tests/__mocks__/setupTests.ts"
]
}
-}
+} \ No newline at end of file
diff --git a/packages/anastasis-webui/preact.config.js b/packages/anastasis-webui/preact.config.js
new file mode 100644
index 000000000..8d6da1911
--- /dev/null
+++ b/packages/anastasis-webui/preact.config.js
@@ -0,0 +1,45 @@
+/*
+ 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 { DefinePlugin } from 'webpack';
+
+import pack from './package.json';
+import * as cp from 'child_process';
+
+const commitHash = cp.execSync('git rev-parse --short HEAD').toString();
+
+export default {
+ webpack(config, env, helpers) {
+ // add __VERSION__ to be use in the html
+ config.plugins.push(
+ new DefinePlugin({
+ 'process.env.__VERSION__': JSON.stringify(env.isProd ? pack.version : `dev-${commitHash}`),
+ }),
+ );
+ const crittersWrapper = helpers.getPluginsByName(config, 'Critters')
+ if (crittersWrapper && crittersWrapper.length > 0) {
+ const [{ index }] = crittersWrapper
+ config.plugins.splice(index, 1)
+ }
+
+ }
+}
+
diff --git a/packages/anastasis-webui/src/.babelrc b/packages/anastasis-webui/src/.babelrc
index 123002210..05f4dcc81 100644
--- a/packages/anastasis-webui/src/.babelrc
+++ b/packages/anastasis-webui/src/.babelrc
@@ -1,5 +1,3 @@
{
- "presets": [
- "preact-cli/babel"
- ]
+ "presets": ["preact-cli/babel"]
}
diff --git a/packages/anastasis-webui/src/components/AsyncButton.tsx b/packages/anastasis-webui/src/components/AsyncButton.tsx
index 92bef2219..8f855f29f 100644
--- a/packages/anastasis-webui/src/components/AsyncButton.tsx
+++ b/packages/anastasis-webui/src/components/AsyncButton.tsx
@@ -15,11 +15,12 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { ComponentChildren, h, VNode } from "preact";
+import { useLayoutEffect, useRef } from "preact/hooks";
// import { LoadingModal } from "../modal";
import { useAsync } from "../hooks/async";
// import { Translate } from "../../i18n";
@@ -28,22 +29,38 @@ type Props = {
children: ComponentChildren;
disabled?: boolean;
onClick?: () => Promise<void>;
+ grabFocus?: boolean;
[rest: string]: any;
};
-export function AsyncButton({ onClick, disabled, children, ...rest }: Props): VNode {
+export function AsyncButton({
+ onClick,
+ grabFocus,
+ disabled,
+ children,
+ ...rest
+}: Props): VNode {
const { isLoading, request } = useAsync(onClick);
+ const buttonRef = useRef<HTMLButtonElement>(null);
+ useLayoutEffect(() => {
+ if (grabFocus) {
+ buttonRef.current?.focus();
+ }
+ }, [grabFocus]);
+
// if (isSlow) {
// return <LoadingModal onCancel={cancel} />;
// }
- if (isLoading) {
+ if (isLoading) {
return <button class="button">Loading...</button>;
}
- return <span data-tooltip={rest['data-tooltip']} style={{marginLeft: 5}}>
- <button {...rest} onClick={request} disabled={disabled}>
- {children}
- </button>
- </span>;
+ return (
+ <span data-tooltip={rest["data-tooltip"]} style={{ marginLeft: 5 }}>
+ <button {...rest} ref={buttonRef} onClick={request} disabled={disabled}>
+ {children}
+ </button>
+ </span>
+ );
}
diff --git a/packages/anastasis-webui/src/components/Notifications.tsx b/packages/anastasis-webui/src/components/Notifications.tsx
index c916020d7..e34550386 100644
--- a/packages/anastasis-webui/src/components/Notifications.tsx
+++ b/packages/anastasis-webui/src/components/Notifications.tsx
@@ -15,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
@@ -27,7 +27,7 @@ export interface Notification {
type: MessageType;
}
-export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
+export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
interface Props {
notifications: Notification[];
@@ -36,24 +36,39 @@ interface Props {
function messageStyle(type: MessageType): string {
switch (type) {
- case "INFO": return "message is-info";
- case "WARN": return "message is-warning";
- case "ERROR": return "message is-danger";
- case "SUCCESS": return "message is-success";
- default: return "message"
+ case "INFO":
+ return "message is-info";
+ case "WARN":
+ return "message is-warning";
+ case "ERROR":
+ return "message is-danger";
+ case "SUCCESS":
+ return "message is-success";
+ default:
+ return "message";
}
}
-export function Notifications({ notifications, removeNotification }: Props): VNode {
- return <div class="block">
- {notifications.map((n,i) => <article key={i} class={messageStyle(n.type)}>
- <div class="message-header">
- <p>{n.message}</p>
- <button class="delete" onClick={() => removeNotification && removeNotification(n)} />
- </div>
- {n.description && <div class="message-body">
- {n.description}
- </div>}
- </article>)}
- </div>
-} \ No newline at end of file
+export function Notifications({
+ notifications,
+ removeNotification,
+}: Props): VNode {
+ return (
+ <div class="block">
+ {notifications.map((n, i) => (
+ <article key={i} class={messageStyle(n.type)}>
+ <div class="message-header">
+ <p>{n.message}</p>
+ {removeNotification && (
+ <button
+ class="delete"
+ onClick={() => removeNotification && removeNotification(n)}
+ />
+ )}
+ </div>
+ {n.description && <div class="message-body">{n.description}</div>}
+ </article>
+ ))}
+ </div>
+ );
+}
diff --git a/packages/anastasis-webui/src/components/QR.tsx b/packages/anastasis-webui/src/components/QR.tsx
index 48f1a7c12..9a05f6097 100644
--- a/packages/anastasis-webui/src/components/QR.tsx
+++ b/packages/anastasis-webui/src/components/QR.tsx
@@ -21,15 +21,28 @@ import qrcode from "qrcode-generator";
export function QR({ text }: { text: string }): VNode {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
- const qr = qrcode(0, 'L');
+ const qr = qrcode(0, "L");
qr.addData(text);
qr.make();
- if (divRef.current) divRef.current.innerHTML = qr.createSvgTag({
- scalable: true,
- });
+ if (divRef.current)
+ divRef.current.innerHTML = qr.createSvgTag({
+ scalable: true,
+ });
});
- return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
- <div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} />
- </div>;
+ 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/anastasis-webui/src/components/app.tsx b/packages/anastasis-webui/src/components/app.tsx
index c6b4cfc14..4c6683c0c 100644
--- a/packages/anastasis-webui/src/components/app.tsx
+++ b/packages/anastasis-webui/src/components/app.tsx
@@ -1,6 +1,5 @@
import { FunctionalComponent, h } from "preact";
import { TranslationProvider } from "../context/translation";
-
import AnastasisClient from "../pages/home";
const App: FunctionalComponent = () => {
diff --git a/packages/anastasis-webui/src/components/fields/DateInput.tsx b/packages/anastasis-webui/src/components/fields/DateInput.tsx
index 3148c953f..18ef89908 100644
--- a/packages/anastasis-webui/src/components/fields/DateInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/DateInput.tsx
@@ -1,4 +1,4 @@
-import { format, isAfter, parse, sub, subYears } from "date-fns";
+import { format, subYears } from "date-fns";
import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import { DatePicker } from "../picker/DatePicker";
@@ -9,6 +9,7 @@ export interface DateInputProps {
tooltip?: string;
error?: string;
years?: Array<number>;
+ onConfirm?: () => void;
bind: [string, (x: string) => void];
}
@@ -19,56 +20,71 @@ export function DateInput(props: DateInputProps): VNode {
inputRef.current?.focus();
}
}, [props.grabFocus]);
- const [opened, setOpened] = useState(false)
+ const [opened, setOpened] = useState(false);
const value = props.bind[0] || "";
- const [dirty, setDirty] = useState(false)
- const showError = dirty && props.error
+ const [dirty, setDirty] = useState(false);
+ const showError = dirty && props.error;
- const calendar = subYears(new Date(), 30)
-
- return <div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control">
- <div class="field has-addons">
- <p class="control">
- <input
- type="text"
- class={showError ? 'input is-danger' : 'input'}
- value={value}
- onInput={(e) => {
- const text = e.currentTarget.value
- setDirty(true)
- props.bind[1](text);
- }}
- ref={inputRef} />
- </p>
- <p class="control">
- <a class="button" onClick={() => { setOpened(true) }}>
- <span class="icon"><i class="mdi mdi-calendar" /></span>
- </a>
- </p>
+ const calendar = subYears(new Date(), 30);
+
+ return (
+ <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control">
+ <div class="field has-addons">
+ <p class="control">
+ <input
+ type="text"
+ class={showError ? "input is-danger" : "input"}
+ value={value}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ onInput={(e) => {
+ const text = e.currentTarget.value;
+ setDirty(true);
+ props.bind[1](text);
+ }}
+ ref={inputRef}
+ />
+ </p>
+ <p class="control">
+ <a
+ class="button"
+ onClick={() => {
+ setOpened(true);
+ }}
+ >
+ <span class="icon">
+ <i class="mdi mdi-calendar" />
+ </span>
+ </a>
+ </p>
+ </div>
</div>
+ <p class="help">Using the format yyyy-mm-dd</p>
+ {showError && <p class="help is-danger">{props.error}</p>}
+ <DatePicker
+ opened={opened}
+ initialDate={calendar}
+ years={props.years}
+ closeFunction={() => setOpened(false)}
+ dateReceiver={(d) => {
+ setDirty(true);
+ const v = format(d, "yyyy-MM-dd");
+ props.bind[1](v);
+ }}
+ />
</div>
- <p class="help">Using the format yyyy-mm-dd</p>
- {showError && <p class="help is-danger">{props.error}</p>}
- <DatePicker
- opened={opened}
- initialDate={calendar}
- years={props.years}
- closeFunction={() => setOpened(false)}
- dateReceiver={(d) => {
- setDirty(true)
- const v = format(d, 'yyyy-MM-dd')
- props.bind[1](v);
- }}
- />
- </div>
- ;
-
+ );
}
diff --git a/packages/anastasis-webui/src/components/fields/EmailInput.tsx b/packages/anastasis-webui/src/components/fields/EmailInput.tsx
index e21418fea..4c35c0686 100644
--- a/packages/anastasis-webui/src/components/fields/EmailInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/EmailInput.tsx
@@ -7,6 +7,7 @@ export interface TextInputProps {
error?: string;
placeholder?: string;
tooltip?: string;
+ onConfirm?: () => void;
bind: [string, (x: string) => void];
}
@@ -18,27 +19,39 @@ export function EmailInput(props: TextInputProps): VNode {
}
}, [props.grabFocus]);
const value = props.bind[0];
- const [dirty, setDirty] = useState(false)
- const showError = dirty && props.error
- return (<div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control has-icons-right">
- <input
- value={value}
- required
- placeholder={props.placeholder}
- type="email"
- class={showError ? 'input is-danger' : 'input'}
- onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
- ref={inputRef}
- style={{ display: "block" }} />
+ const [dirty, setDirty] = useState(false);
+ const showError = dirty && props.error;
+ return (
+ <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ value={value}
+ required
+ placeholder={props.placeholder}
+ type="email"
+ class={showError ? "input is-danger" : "input"}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ onInput={(e) => {
+ setDirty(true);
+ props.bind[1]((e.target as HTMLInputElement).value);
+ }}
+ ref={inputRef}
+ style={{ display: "block" }}
+ />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
</div>
- {showError && <p class="help is-danger">{props.error}</p>}
- </div>
);
}
diff --git a/packages/anastasis-webui/src/components/fields/FileInput.tsx b/packages/anastasis-webui/src/components/fields/FileInput.tsx
index 8b144ea43..adf51afb0 100644
--- a/packages/anastasis-webui/src/components/fields/FileInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/FileInput.tsx
@@ -15,16 +15,31 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
-import { TextInputProps } from "./TextInput";
-const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
+const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
+
+export interface FileTypeContent {
+ content: string;
+ type: string;
+ name: string;
+}
+
+export interface FileInputProps {
+ label: string;
+ grabFocus?: boolean;
+ disabled?: boolean;
+ error?: string;
+ placeholder?: string;
+ tooltip?: string;
+ onChange: (v: FileTypeContent | undefined) => void;
+}
-export function FileInput(props: TextInputProps): VNode {
+export function FileInput(props: FileInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null);
useLayoutEffect(() => {
if (props.grabFocus) {
@@ -32,50 +47,58 @@ export function FileInput(props: TextInputProps): VNode {
}
}, [props.grabFocus]);
- const value = props.bind[0];
- // const [dirty, setDirty] = useState(false)
- const image = useRef<HTMLInputElement>(null)
- const [sizeError, setSizeError] = useState(false)
- function onChange(v: string): void {
- // setDirty(true);
- props.bind[1](v);
- }
- return <div class="field">
- <label class="label">
- <a onClick={() => image.current?.click()}>
- {props.label}
- </a>
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control">
- <input
- ref={image} style={{ display: 'none' }}
- type="file" name={String(name)}
- onChange={e => {
- const f: FileList | null = e.currentTarget.files
- if (!f || f.length != 1) {
- return onChange("")
- }
- if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
- setSizeError(true)
- return onChange("")
- }
- setSizeError(false)
- return f[0].arrayBuffer().then(b => {
- const b64 = btoa(
- new Uint8Array(b)
- .reduce((data, byte) => data + String.fromCharCode(byte), '')
- )
- return onChange(`data:${f[0].type};base64,${b64}` as any)
- })
- }} />
- {props.error && <p class="help is-danger">{props.error}</p>}
- {sizeError && <p class="help is-danger">
- File should be smaller than 1 MB
- </p>}
+ const fileInputRef = useRef<HTMLInputElement>(null);
+ const [sizeError, setSizeError] = useState(false);
+ return (
+ <div class="field">
+ <label class="label">
+ <a class="button" onClick={(e) => fileInputRef.current?.click()}>
+ <div class="icon is-small ">
+ <i class="mdi mdi-folder" />
+ </div>
+ <span>
+ {props.label}
+ </span>
+ </a>
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control">
+ <input
+ ref={fileInputRef}
+ style={{ display: "none" }}
+ type="file"
+ // name={String(name)}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return props.onChange(undefined);
+ }
+ console.log(f)
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true);
+ return props.onChange(undefined);
+ }
+ setSizeError(false);
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return props.onChange({content: `data:${f[0].type};base64,${b64}`, name: f[0].name, type: f[0].type});
+ });
+ }}
+ />
+ {props.error && <p class="help is-danger">{props.error}</p>}
+ {sizeError && (
+ <p class="help is-danger">File should be smaller than 1 MB</p>
+ )}
+ </div>
</div>
- </div>
+ );
}
-
diff --git a/packages/anastasis-webui/src/components/fields/ImageInput.tsx b/packages/anastasis-webui/src/components/fields/ImageInput.tsx
index d5bf643d4..3f8cc58dd 100644
--- a/packages/anastasis-webui/src/components/fields/ImageInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/ImageInput.tsx
@@ -15,15 +15,15 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import emptyImage from "../../assets/empty.png";
import { TextInputProps } from "./TextInput";
-const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
+const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
export function ImageInput(props: TextInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null);
@@ -35,47 +35,59 @@ export function ImageInput(props: TextInputProps): VNode {
const value = props.bind[0];
// const [dirty, setDirty] = useState(false)
- const image = useRef<HTMLInputElement>(null)
- const [sizeError, setSizeError] = useState(false)
+ const image = useRef<HTMLInputElement>(null);
+ const [sizeError, setSizeError] = useState(false);
function onChange(v: string): void {
// setDirty(true);
props.bind[1](v);
}
- return <div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control">
- <img src={!value ? emptyImage : value} style={{ width: 200, height: 200 }} onClick={() => image.current?.click()} />
- <input
- ref={image} style={{ display: 'none' }}
- type="file" name={String(name)}
- onChange={e => {
- const f: FileList | null = e.currentTarget.files
- if (!f || f.length != 1) {
- return onChange(emptyImage)
- }
- if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
- setSizeError(true)
- return onChange(emptyImage)
- }
- setSizeError(false)
- return f[0].arrayBuffer().then(b => {
- const b64 = btoa(
- new Uint8Array(b)
- .reduce((data, byte) => data + String.fromCharCode(byte), '')
- )
- return onChange(`data:${f[0].type};base64,${b64}` as any)
- })
- }} />
- {props.error && <p class="help is-danger">{props.error}</p>}
- {sizeError && <p class="help is-danger">
- Image should be smaller than 1 MB
- </p>}
+ return (
+ <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control">
+ <img
+ src={!value ? emptyImage : value}
+ style={{ width: 200, height: 200 }}
+ onClick={() => image.current?.click()}
+ />
+ <input
+ ref={image}
+ style={{ display: "none" }}
+ type="file"
+ name={String(name)}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return onChange(emptyImage);
+ }
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true);
+ return onChange(emptyImage);
+ }
+ setSizeError(false);
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return onChange(`data:${f[0].type};base64,${b64}` as any);
+ });
+ }}
+ />
+ {props.error && <p class="help is-danger">{props.error}</p>}
+ {sizeError && (
+ <p class="help is-danger">Image should be smaller than 1 MB</p>
+ )}
+ </div>
</div>
- </div>
+ );
}
-
diff --git a/packages/anastasis-webui/src/components/fields/NumberInput.tsx b/packages/anastasis-webui/src/components/fields/NumberInput.tsx
index 2afb242b8..4856131c7 100644
--- a/packages/anastasis-webui/src/components/fields/NumberInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/NumberInput.tsx
@@ -7,10 +7,11 @@ export interface TextInputProps {
error?: string;
placeholder?: string;
tooltip?: string;
+ onConfirm?: () => void;
bind: [string, (x: string) => void];
}
-export function NumberInput(props: TextInputProps): VNode {
+export function PhoneNumberInput(props: TextInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null);
useLayoutEffect(() => {
if (props.grabFocus) {
@@ -18,26 +19,38 @@ export function NumberInput(props: TextInputProps): VNode {
}
}, [props.grabFocus]);
const value = props.bind[0];
- const [dirty, setDirty] = useState(false)
- const showError = dirty && props.error
- return (<div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control has-icons-right">
- <input
- value={value}
- type="number"
- placeholder={props.placeholder}
- class={showError ? 'input is-danger' : 'input'}
- onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
- ref={inputRef}
- style={{ display: "block" }} />
+ const [dirty, setDirty] = useState(false);
+ const showError = dirty && props.error;
+ return (
+ <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ value={value}
+ type="tel"
+ placeholder={props.placeholder}
+ class={showError ? "input is-danger" : "input"}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ onInput={(e) => {
+ setDirty(true);
+ props.bind[1]((e.target as HTMLInputElement).value);
+ }}
+ ref={inputRef}
+ style={{ display: "block" }}
+ />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
</div>
- {showError && <p class="help is-danger">{props.error}</p>}
- </div>
);
}
diff --git a/packages/anastasis-webui/src/components/fields/TextInput.tsx b/packages/anastasis-webui/src/components/fields/TextInput.tsx
index c093689c5..efa95d84e 100644
--- a/packages/anastasis-webui/src/components/fields/TextInput.tsx
+++ b/packages/anastasis-webui/src/components/fields/TextInput.tsx
@@ -4,9 +4,11 @@ import { useLayoutEffect, useRef, useState } from "preact/hooks";
export interface TextInputProps {
label: string;
grabFocus?: boolean;
+ disabled?: boolean;
error?: string;
placeholder?: string;
tooltip?: string;
+ onConfirm?: () => void;
bind: [string, (x: string) => void];
}
@@ -18,25 +20,38 @@ export function TextInput(props: TextInputProps): VNode {
}
}, [props.grabFocus]);
const value = props.bind[0];
- const [dirty, setDirty] = useState(false)
- const showError = dirty && props.error
- return (<div class="field">
- <label class="label">
- {props.label}
- {props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
- <i class="mdi mdi-information" />
- </span>}
- </label>
- <div class="control has-icons-right">
- <input
- value={value}
- placeholder={props.placeholder}
- class={showError ? 'input is-danger' : 'input'}
- onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
- ref={inputRef}
- style={{ display: "block" }} />
+ const [dirty, setDirty] = useState(false);
+ const showError = dirty && props.error;
+ return (
+ <div class="field">
+ <label class="label">
+ {props.label}
+ {props.tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ <div class="control has-icons-right">
+ <input
+ value={value}
+ disabled={props.disabled}
+ placeholder={props.placeholder}
+ class={showError ? "input is-danger" : "input"}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && props.onConfirm) {
+ props.onConfirm()
+ }
+ }}
+ onInput={(e) => {
+ setDirty(true);
+ props.bind[1]((e.target as HTMLInputElement).value);
+ }}
+ ref={inputRef}
+ style={{ display: "block" }}
+ />
+ </div>
+ {showError && <p class="help is-danger">{props.error}</p>}
</div>
- {showError && <p class="help is-danger">{props.error}</p>}
- </div>
);
}
diff --git a/packages/anastasis-webui/src/components/menu/LangSelector.tsx b/packages/anastasis-webui/src/components/menu/LangSelector.tsx
index 0f91abd7e..fa22a29c0 100644
--- a/packages/anastasis-webui/src/components/menu/LangSelector.tsx
+++ b/packages/anastasis-webui/src/components/menu/LangSelector.tsx
@@ -15,59 +15,78 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import langIcon from '../../assets/icons/languageicon.svg';
+import langIcon from "../../assets/icons/languageicon.svg";
import { useTranslationContext } from "../../context/translation";
-import { strings as messages } from '../../i18n/strings'
+import { strings as messages } from "../../i18n/strings";
type LangsNames = {
- [P in keyof typeof messages]: string
-}
+ [P in keyof typeof messages]: string;
+};
const names: LangsNames = {
- es: 'Español [es]',
- en: 'English [en]',
- fr: 'Français [fr]',
- de: 'Deutsch [de]',
- sv: 'Svenska [sv]',
- it: 'Italiano [it]',
-}
+ es: "Español [es]",
+ en: "English [en]",
+ fr: "Français [fr]",
+ de: "Deutsch [de]",
+ sv: "Svenska [sv]",
+ it: "Italiano [it]",
+};
function getLangName(s: keyof LangsNames | string): string {
- if (names[s]) return names[s]
- return String(s)
+ if (names[s]) return names[s];
+ return String(s);
}
export function LangSelector(): VNode {
- const [updatingLang, setUpdatingLang] = useState(false)
- const { lang, changeLanguage } = useTranslationContext()
+ const [updatingLang, setUpdatingLang] = useState(false);
+ const { lang, changeLanguage } = useTranslationContext();
- return <div class="dropdown is-active ">
- <div class="dropdown-trigger">
- <button class="button has-tooltip-left"
- data-tooltip="change language selection"
- aria-haspopup="true"
- aria-controls="dropdown-menu" onClick={() => setUpdatingLang(!updatingLang)}>
- <div class="icon is-small is-left">
- <img src={langIcon} />
- </div>
- <span>{getLangName(lang)}</span>
- <div class="icon is-right">
- <i class="mdi mdi-chevron-down" />
+ return (
+ <div class="dropdown is-active ">
+ <div class="dropdown-trigger">
+ <button
+ class="button has-tooltip-left"
+ data-tooltip="change language selection"
+ aria-haspopup="true"
+ aria-controls="dropdown-menu"
+ onClick={() => setUpdatingLang(!updatingLang)}
+ >
+ <div class="icon is-small is-left">
+ <img src={langIcon} />
+ </div>
+ <span>{getLangName(lang)}</span>
+ <div class="icon is-right">
+ <i class="mdi mdi-chevron-down" />
+ </div>
+ </button>
+ </div>
+ {updatingLang && (
+ <div class="dropdown-menu" id="dropdown-menu" role="menu">
+ <div class="dropdown-content">
+ {Object.keys(messages)
+ .filter((l) => l !== lang)
+ .map((l) => (
+ <a
+ key={l}
+ class="dropdown-item"
+ value={l}
+ onClick={() => {
+ changeLanguage(l);
+ setUpdatingLang(false);
+ }}
+ >
+ {getLangName(l)}
+ </a>
+ ))}
+ </div>
</div>
- </button>
+ )}
</div>
- {updatingLang && <div class="dropdown-menu" id="dropdown-menu" role="menu">
- <div class="dropdown-content">
- {Object.keys(messages)
- .filter((l) => l !== lang)
- .map(l => <a key={l} class="dropdown-item" value={l} onClick={() => { changeLanguage(l); setUpdatingLang(false) }}>{getLangName(l)}</a>)}
- </div>
- </div>}
- </div>
-} \ No newline at end of file
+ );
+}
diff --git a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
index 935951ab9..8d5a0473b 100644
--- a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx
@@ -15,13 +15,13 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { h, VNode } from 'preact';
-import logo from '../../assets/logo.jpeg';
-import { LangSelector } from './LangSelector';
+import { h, VNode } from "preact";
+import logo from "../../assets/logo.jpeg";
+import { LangSelector } from "./LangSelector";
interface Props {
onMobileMenu: () => void;
@@ -29,30 +29,51 @@ interface Props {
}
export function NavigationBar({ onMobileMenu, title }: Props): VNode {
- return (<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
- <div class="navbar-brand">
- <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>{title}</span>
-
- <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" onClick={(e) => {
- onMobileMenu()
- e.stopPropagation()
- }}>
- <span aria-hidden="true" />
- <span aria-hidden="true" />
- <span aria-hidden="true" />
- </a>
- </div>
-
- <div class="navbar-menu ">
- <a class="navbar-start is-justify-content-center is-flex-grow-1" href="https://taler.net">
- <img src={logo} style={{ height: 50, maxHeight: 50 }} />
- </a>
- <div class="navbar-end">
- <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
- {/* <LangSelector /> */}
+ return (
+ <nav
+ class="navbar is-fixed-top"
+ role="navigation"
+ aria-label="main navigation"
+ >
+ <div class="navbar-brand">
+ <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>
+ {title}
+ </span>
+ <a
+ href="mailto:contact@anastasis.lu"
+ style={{ alignSelf: "center", padding: "0.5em" }}
+ >
+ Contact us
+ </a>
+ <a
+ href="https://bugs.anastasis.li/"
+ style={{ alignSelf: "center", padding: "0.5em" }}
+ >
+ Report a bug
+ </a>
+ {/* <a
+ role="button"
+ class="navbar-burger"
+ aria-label="menu"
+ aria-expanded="false"
+ onClick={(e) => {
+ onMobileMenu();
+ e.stopPropagation();
+ }}
+ >
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ <span aria-hidden="true" />
+ </a> */}
+ </div>
+
+ <div class="navbar-menu ">
+ <div class="navbar-end">
+ <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
+ {/* <LangSelector /> */}
+ </div>
</div>
</div>
- </div>
- </nav>
+ </nav>
);
-} \ No newline at end of file
+}
diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx
index 72655662f..c73369dd6 100644
--- a/packages/anastasis-webui/src/components/menu/SideBar.tsx
+++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx
@@ -15,16 +15,15 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { Fragment, h, VNode } from 'preact';
-import { BackupStates, RecoveryStates } from '../../../../anastasis-core/lib';
-import { useAnastasisContext } from '../../context/anastasis';
-import { Translate } from '../../i18n';
-import { LangSelector } from './LangSelector';
+import { Fragment, h, VNode } from "preact";
+import { BackupStates, RecoveryStates } from "../../../../anastasis-core/lib";
+import { useAnastasisContext } from "../../context/anastasis";
+import { Translate } from "../../i18n";
+import { LangSelector } from "./LangSelector";
interface Props {
mobile?: boolean;
@@ -32,10 +31,10 @@ interface Props {
export function Sidebar({ mobile }: Props): VNode {
// const config = useConfigContext();
- const config = { version: 'none' }
+ const config = { version: "none" };
// FIXME: add replacement for __VERSION__ with the current version
- const process = { env: { __VERSION__: '0.0.0' } }
- const reducer = useAnastasisContext()!
+ const process = { env: { __VERSION__: "0.0.0" } };
+ const reducer = useAnastasisContext()!;
return (
<aside class="aside is-placed-left is-expanded">
@@ -44,114 +43,235 @@ export function Sidebar({ mobile }: Props): VNode {
</div>} */}
<div class="aside-tools">
<div class="aside-tools-label">
- <div><b>Anastasis</b> Reducer</div>
- <div class="is-size-7 has-text-right" style={{ lineHeight: 0, marginTop: -10 }}>
- {process.env.__VERSION__} ({config.version})
+ <div>
+ <b>Anastasis</b>
+ </div>
+ <div
+ class="is-size-7 has-text-right"
+ style={{ lineHeight: 0, marginTop: -10 }}
+ >
+ Version {process.env.__VERSION__} ({config.version})
</div>
</div>
</div>
<div class="menu is-menu-main">
- {!reducer.currentReducerState &&
+ {!reducer.currentReducerState && (
<p class="menu-label">
<Translate>Backup or Recorver</Translate>
</p>
- }
+ )}
<ul class="menu-list">
- {!reducer.currentReducerState &&
+ {!reducer.currentReducerState && (
<li>
<div class="ml-4">
- <span class="menu-item-label"><Translate>Select one option</Translate></span>
- </div>
- </li>
- }
- {reducer.currentReducerState && reducer.currentReducerState.backup_state ? <Fragment>
- <li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
- reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Location</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Personal information</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}>
- <div class="ml-4">
-
- <span class="menu-item-label"><Translate>Authorization methods</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}>
- <div class="ml-4">
-
- <span class="menu-item-label"><Translate>Policies</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}>
- <div class="ml-4">
-
- <span class="menu-item-label"><Translate>Secret input</Translate></span>
+ <span class="menu-item-label">
+ <Translate>Select one option</Translate>
+ </span>
</div>
</li>
- {/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
+ )}
+ {reducer.currentReducerState &&
+ reducer.currentReducerState.backup_state ? (
+ <Fragment>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.ContinentSelecting ||
+ reducer.currentReducerState.backup_state ===
+ BackupStates.CountrySelecting
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Location</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.UserAttributesCollecting
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Personal information</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.AuthenticationsEditing
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Authorization methods</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.PoliciesReviewing
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Policies</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.SecretEditing
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Secret input</Translate>
+ </span>
+ </div>
+ </li>
+ {/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Payment (optional)</Translate></span>
</div>
</li> */}
- <li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}>
- <div class="ml-4">
-
- <span class="menu-item-label"><Translate>Backup completed</Translate></span>
- </div>
- </li>
- {/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
+ <li
+ class={
+ reducer.currentReducerState.backup_state ===
+ BackupStates.BackupFinished
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Backup completed</Translate>
+ </span>
+ </div>
+ </li>
+ {/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Truth Paying</Translate></span>
</div>
</li> */}
- </Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ||
- reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Location</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Personal information</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.SecretSelecting ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Secret selection</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSelecting ||
- reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSolving ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Solve Challenges</Translate></span>
- </div>
- </li>
- <li class={reducer.currentReducerState.recovery_state === RecoveryStates.RecoveryFinished ? 'is-active' : ''}>
- <div class="ml-4">
- <span class="menu-item-label"><Translate>Secret recovered</Translate></span>
- </div>
- </li>
- </Fragment>)}
- {reducer.currentReducerState &&
+ </Fragment>
+ ) : (
+ reducer.currentReducerState &&
+ reducer.currentReducerState?.recovery_state && (
+ <Fragment>
+ <li
+ class={
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.ContinentSelecting ||
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.CountrySelecting
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Location</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.UserAttributesCollecting
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Personal information</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.SecretSelecting
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Secret selection</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.ChallengeSelecting ||
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.ChallengeSolving
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Solve Challenges</Translate>
+ </span>
+ </div>
+ </li>
+ <li
+ class={
+ reducer.currentReducerState.recovery_state ===
+ RecoveryStates.RecoveryFinished
+ ? "is-active"
+ : ""
+ }
+ >
+ <div class="ml-4">
+ <span class="menu-item-label">
+ <Translate>Secret recovered</Translate>
+ </span>
+ </div>
+ </li>
+ </Fragment>
+ )
+ )}
+ {reducer.currentReducerState && (
<li>
<div class="buttons ml-4">
- <button class="button is-danger is-right" onClick={() => reducer.reset()}>Reset session</button>
+ <button
+ class="button is-danger is-right"
+ onClick={() => reducer.reset()}
+ >
+ Reset session
+ </button>
</div>
</li>
- }
-
+ )}
+ {/* <li>
+ <div class="buttons ml-4">
+ <button class="button is-info is-right" >Manage providers</button>
+ </div>
+ </li> */}
</ul>
</div>
</aside>
);
}
-
diff --git a/packages/anastasis-webui/src/components/menu/index.tsx b/packages/anastasis-webui/src/components/menu/index.tsx
index febcd79c8..99d0f7646 100644
--- a/packages/anastasis-webui/src/components/menu/index.tsx
+++ b/packages/anastasis-webui/src/components/menu/index.tsx
@@ -15,41 +15,53 @@
*/
import { ComponentChildren, Fragment, h, VNode } from "preact";
-import Match from 'preact-router/match';
+import Match from "preact-router/match";
import { useEffect, useState } from "preact/hooks";
import { NavigationBar } from "./NavigationBar";
import { Sidebar } from "./SideBar";
-
-
-
interface MenuProps {
title: string;
}
-function WithTitle({ title, children }: { title: string; children: ComponentChildren }): VNode {
+function WithTitle({
+ title,
+ children,
+}: {
+ title: string;
+ children: ComponentChildren;
+}): VNode {
useEffect(() => {
- document.title = `Taler Backoffice: ${title}`
- }, [title])
- return <Fragment>{children}</Fragment>
+ document.title = `${title}`;
+ }, [title]);
+ return <Fragment>{children}</Fragment>;
}
export function Menu({ title }: MenuProps): VNode {
- const [mobileOpen, setMobileOpen] = useState(false)
-
- return <Match>{({ path }: { path: string }) => {
- const titleWithSubtitle = title // title ? title : (!admin ? getInstanceTitle(path, instance) : getAdminTitle(path, instance))
- return (<WithTitle title={titleWithSubtitle}>
- <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}>
- <NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={titleWithSubtitle} />
-
- <Sidebar mobile={mobileOpen} />
-
- </div>
- </WithTitle>
- )
- }}</Match>
-
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ return (
+ <Match>
+ {({ path }: { path: string }) => {
+ const titleWithSubtitle = title; // title ? title : (!admin ? getInstanceTitle(path, instance) : getAdminTitle(path, instance))
+ return (
+ <WithTitle title={titleWithSubtitle}>
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={titleWithSubtitle}
+ />
+
+ <Sidebar mobile={mobileOpen} />
+ </div>
+ </WithTitle>
+ );
+ }}
+ </Match>
+ );
}
interface NotYetReadyAppMenuProps {
@@ -60,37 +72,57 @@ interface NotYetReadyAppMenuProps {
interface NotifProps {
notification?: Notification;
}
-export function NotificationCard({ notification: n }: NotifProps): VNode | null {
- if (!n) return null
- return <div class="notification">
- <div class="columns is-vcentered">
- <div class="column is-12">
- <article class={n.type === 'ERROR' ? "message is-danger" : (n.type === 'WARN' ? "message is-warning" : "message is-info")}>
- <div class="message-header">
- <p>{n.message}</p>
- </div>
- {n.description &&
- <div class="message-body">
- {n.description}
- </div>}
- </article>
+export function NotificationCard({
+ notification: n,
+}: NotifProps): VNode | null {
+ if (!n) return null;
+ return (
+ <div class="notification">
+ <div class="columns is-vcentered">
+ <div class="column is-12">
+ <article
+ class={
+ n.type === "ERROR"
+ ? "message is-danger"
+ : n.type === "WARN"
+ ? "message is-warning"
+ : "message is-info"
+ }
+ >
+ <div class="message-header">
+ <p>{n.message}</p>
+ </div>
+ {n.description && <div class="message-body">{n.description}</div>}
+ </article>
+ </div>
</div>
</div>
- </div>
+ );
}
-export function NotYetReadyAppMenu({ onLogout, title }: NotYetReadyAppMenuProps): VNode {
- const [mobileOpen, setMobileOpen] = useState(false)
+export function NotYetReadyAppMenu({
+ onLogout,
+ title,
+}: NotYetReadyAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
useEffect(() => {
- document.title = `Taler Backoffice: ${title}`
- }, [title])
-
- return <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}>
- <NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={title} />
- {onLogout && <Sidebar mobile={mobileOpen} />}
- </div>
-
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class="has-aside-mobile-expanded"
+ // class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ {onLogout && <Sidebar mobile={mobileOpen} />}
+ </div>
+ );
}
export interface Notification {
@@ -99,6 +131,5 @@ export interface Notification {
type: MessageType;
}
-export type ValueOrFunction<T> = T | ((p: T) => T)
-export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
-
+export type ValueOrFunction<T> = T | ((p: T) => T);
+export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
diff --git a/packages/anastasis-webui/src/components/picker/DatePicker.tsx b/packages/anastasis-webui/src/components/picker/DatePicker.tsx
index eb5d8145d..d689db386 100644
--- a/packages/anastasis-webui/src/components/picker/DatePicker.tsx
+++ b/packages/anastasis-webui/src/components/picker/DatePicker.tsx
@@ -15,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, Component } from "preact";
@@ -34,83 +34,71 @@ interface State {
selectYearMode: boolean;
currentDate: Date;
}
-const now = new Date()
+const now = new Date();
const monthArrShortFull = [
- 'January',
- 'February',
- 'March',
- 'April',
- 'May',
- 'June',
- 'July',
- 'August',
- 'September',
- 'October',
- 'November',
- 'December'
-]
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+];
const monthArrShort = [
- 'Jan',
- 'Feb',
- 'Mar',
- 'Apr',
- 'May',
- 'Jun',
- 'Jul',
- 'Aug',
- 'Sep',
- 'Oct',
- 'Nov',
- 'Dec'
-]
-
-const dayArr = [
- 'Sun',
- 'Mon',
- 'Tue',
- 'Wed',
- 'Thu',
- 'Fri',
- 'Sat'
-]
-
-const yearArr: number[] = []
-
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+];
+
+const dayArr = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+const yearArr: number[] = [];
// inspired by https://codepen.io/m4r1vs/pen/MOOxyE
export class DatePicker extends Component<Props, State> {
-
closeDatePicker() {
this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent
}
/**
- * Gets fired when a day gets clicked.
- * @param {object} e The event thrown by the <span /> element clicked
- */
+ * Gets fired when a day gets clicked.
+ * @param {object} e The event thrown by the <span /> element clicked
+ */
dayClicked(e: any) {
-
const element = e.target; // the actual element clicked
- if (element.innerHTML === '') return false; // don't continue if <span /> empty
+ if (element.innerHTML === "") return false; // don't continue if <span /> empty
// get date from clicked element (gets attached when rendered)
- const date = new Date(element.getAttribute('data-value'));
+ const date = new Date(element.getAttribute("data-value"));
// update the state
this.setState({ currentDate: date });
- this.passDateToParent(date)
+ this.passDateToParent(date);
}
/**
- * returns days in month as array
- * @param {number} month the month to display
- * @param {number} year the year to display
- */
+ * returns days in month as array
+ * @param {number} month the month to display
+ * @param {number} year the year to display
+ */
getDaysByMonth(month: number, year: number) {
-
const calendar = [];
const date = new Date(year, month, 1); // month to display
@@ -122,15 +110,17 @@ export class DatePicker extends Component<Props, State> {
// the calendar is 7*6 fields big, so 42 loops
for (let i = 0; i < 42; i++) {
-
if (i >= firstDay && day !== null) day = day + 1;
if (day !== null && day > lastDate) day = null;
// append the calendar Array
calendar.push({
- day: (day === 0 || day === null) ? null : day, // null or number
- date: (day === 0 || day === null) ? null : new Date(year, month, day), // null or Date()
- today: (day === now.getDate() && month === now.getMonth() && year === now.getFullYear()) // boolean
+ day: day === 0 || day === null ? null : day, // null or number
+ date: day === 0 || day === null ? null : new Date(year, month, day), // null or Date()
+ today:
+ day === now.getDate() &&
+ month === now.getMonth() &&
+ year === now.getFullYear(), // boolean
});
}
@@ -138,51 +128,48 @@ export class DatePicker extends Component<Props, State> {
}
/**
- * Display previous month by updating state
- */
+ * Display previous month by updating state
+ */
displayPrevMonth() {
if (this.state.displayedMonth <= 0) {
this.setState({
displayedMonth: 11,
- displayedYear: this.state.displayedYear - 1
+ displayedYear: this.state.displayedYear - 1,
});
- }
- else {
+ } else {
this.setState({
- displayedMonth: this.state.displayedMonth - 1
+ displayedMonth: this.state.displayedMonth - 1,
});
}
}
/**
- * Display next month by updating state
- */
+ * Display next month by updating state
+ */
displayNextMonth() {
if (this.state.displayedMonth >= 11) {
this.setState({
displayedMonth: 0,
- displayedYear: this.state.displayedYear + 1
+ displayedYear: this.state.displayedYear + 1,
});
- }
- else {
+ } else {
this.setState({
- displayedMonth: this.state.displayedMonth + 1
+ displayedMonth: this.state.displayedMonth + 1,
});
}
}
/**
- * Display the selected month (gets fired when clicking on the date string)
- */
+ * Display the selected month (gets fired when clicking on the date string)
+ */
displaySelectedMonth() {
if (this.state.selectYearMode) {
this.toggleYearSelector();
- }
- else {
+ } else {
if (!this.state.currentDate) return false;
this.setState({
displayedMonth: this.state.currentDate.getMonth(),
- displayedYear: this.state.currentDate.getFullYear()
+ displayedYear: this.state.currentDate.getFullYear(),
});
}
}
@@ -194,17 +181,21 @@ export class DatePicker extends Component<Props, State> {
changeDisplayedYear(e: any) {
const element = e.target;
this.toggleYearSelector();
- this.setState({ displayedYear: parseInt(element.innerHTML, 10), displayedMonth: 0 });
+ this.setState({
+ displayedYear: parseInt(element.innerHTML, 10),
+ displayedMonth: 0,
+ });
}
/**
- * Pass the selected date to parent when 'OK' is clicked
- */
+ * Pass the selected date to parent when 'OK' is clicked
+ */
passSavedDateDateToParent() {
- this.passDateToParent(this.state.currentDate)
+ this.passDateToParent(this.state.currentDate);
}
passDateToParent(date: Date) {
- if (typeof this.props.dateReceiver === 'function') this.props.dateReceiver(date);
+ if (typeof this.props.dateReceiver === "function")
+ this.props.dateReceiver(date);
this.closeDatePicker();
}
@@ -233,94 +224,133 @@ export class DatePicker extends Component<Props, State> {
currentDate: initial,
displayedMonth: initial.getMonth(),
displayedYear: initial.getFullYear(),
- selectYearMode: false
- }
+ selectYearMode: false,
+ };
}
render() {
-
- const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state;
+ const {
+ currentDate,
+ displayedMonth,
+ displayedYear,
+ selectYearMode,
+ } = this.state;
return (
<div>
- <div class={`datePicker ${ this.props.opened && "datePicker--opened"}`}>
-
+ <div class={`datePicker ${this.props.opened && "datePicker--opened"}`}>
<div class="datePicker--titles">
- <h3 style={{
- color: selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
- }} onClick={this.toggleYearSelector}>{currentDate.getFullYear()}</h3>
- <h2 style={{
- color: !selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
- }} onClick={this.displaySelectedMonth}>
- {dayArr[currentDate.getDay()]}, {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
+ <h3
+ style={{
+ color: selectYearMode
+ ? "rgba(255,255,255,.87)"
+ : "rgba(255,255,255,.57)",
+ }}
+ onClick={this.toggleYearSelector}
+ >
+ {currentDate.getFullYear()}
+ </h3>
+ <h2
+ style={{
+ color: !selectYearMode
+ ? "rgba(255,255,255,.87)"
+ : "rgba(255,255,255,.57)",
+ }}
+ onClick={this.displaySelectedMonth}
+ >
+ {dayArr[currentDate.getDay()]},{" "}
+ {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
</h2>
</div>
- {!selectYearMode && <nav>
- <span onClick={this.displayPrevMonth} class="icon"><i style={{ transform: 'rotate(180deg)' }} class="mdi mdi-forward" /></span>
- <h4>{monthArrShortFull[displayedMonth]} {displayedYear}</h4>
- <span onClick={this.displayNextMonth} class="icon"><i class="mdi mdi-forward" /></span>
- </nav>}
+ {!selectYearMode && (
+ <nav>
+ <span onClick={this.displayPrevMonth} class="icon">
+ <i
+ style={{ transform: "rotate(180deg)" }}
+ class="mdi mdi-forward"
+ />
+ </span>
+ <h4>
+ {monthArrShortFull[displayedMonth]} {displayedYear}
+ </h4>
+ <span onClick={this.displayNextMonth} class="icon">
+ <i class="mdi mdi-forward" />
+ </span>
+ </nav>
+ )}
<div class="datePicker--scroll">
-
- {!selectYearMode && <div class="datePicker--calendar" >
-
- <div class="datePicker--dayNames">
- {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day,i) => <span key={i}>{day}</span>)}
- </div>
-
- <div onClick={this.dayClicked} class="datePicker--days">
-
- {/*
+ {!selectYearMode && (
+ <div class="datePicker--calendar">
+ <div class="datePicker--dayNames">
+ {["S", "M", "T", "W", "T", "F", "S"].map((day, i) => (
+ <span key={i}>{day}</span>
+ ))}
+ </div>
+
+ <div onClick={this.dayClicked} class="datePicker--days">
+ {/*
Loop through the calendar object returned by getDaysByMonth().
*/}
- {this.getDaysByMonth(this.state.displayedMonth, this.state.displayedYear)
- .map(
- day => {
- let selected = false;
-
- if (currentDate && day.date) selected = (currentDate.toLocaleDateString() === day.date.toLocaleDateString());
-
- return (<span key={day.day}
- class={(day.today ? 'datePicker--today ' : '') + (selected ? 'datePicker--selected' : '')}
+ {this.getDaysByMonth(
+ this.state.displayedMonth,
+ this.state.displayedYear,
+ ).map((day) => {
+ let selected = false;
+
+ if (currentDate && day.date)
+ selected =
+ currentDate.toLocaleDateString() ===
+ day.date.toLocaleDateString();
+
+ return (
+ <span
+ key={day.day}
+ class={
+ (day.today ? "datePicker--today " : "") +
+ (selected ? "datePicker--selected" : "")
+ }
disabled={!day.date}
data-value={day.date}
>
{day.day}
- </span>)
- }
- )
- }
-
+ </span>
+ );
+ })}
+ </div>
</div>
-
- </div>}
-
- {selectYearMode && <div class="datePicker--selectYear">
- {(this.props.years || yearArr).map(year => (
- <span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}>
- {year}
- </span>
- ))}
-
- </div>}
-
+ )}
+
+ {selectYearMode && (
+ <div class="datePicker--selectYear">
+ {(this.props.years || yearArr).map((year) => (
+ <span
+ key={year}
+ class={year === displayedYear ? "selected" : ""}
+ onClick={this.changeDisplayedYear}
+ >
+ {year}
+ </span>
+ ))}
+ </div>
+ )}
</div>
</div>
- <div class="datePicker--background" onClick={this.closeDatePicker} style={{
- display: this.props.opened ? 'block' : 'none',
- }}
+ <div
+ class="datePicker--background"
+ onClick={this.closeDatePicker}
+ style={{
+ display: this.props.opened ? "block" : "none",
+ }}
/>
-
</div>
- )
+ );
}
}
-
for (let i = 2010; i <= now.getFullYear() + 10; i++) {
yearArr.push(i);
}
diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
index 275c80fa6..7f96cc15b 100644
--- a/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
+++ b/packages/anastasis-webui/src/components/picker/DurationPicker.stories.tsx
@@ -15,36 +15,41 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { h, FunctionalComponent } from 'preact';
-import { useState } from 'preact/hooks';
-import { DurationPicker as TestedComponent } from './DurationPicker';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, FunctionalComponent } from "preact";
+import { useState } from "preact/hooks";
+import { DurationPicker as TestedComponent } from "./DurationPicker";
export default {
- title: 'Components/Picker/Duration',
+ title: "Components/Picker/Duration",
component: TestedComponent,
argTypes: {
- onCreate: { action: 'onCreate' },
- goBack: { action: 'goBack' },
- }
+ onCreate: { action: "onCreate" },
+ goBack: { action: "goBack" },
+ },
};
-function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
- const r = (args: any) => <Component {...args} />
- r.args = props
- return r
+function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => <Component {...args} />;
+ r.args = props;
+ return r;
}
export const Example = createExample(TestedComponent, {
- days: true, minutes: true, hours: true, seconds: true,
- value: 10000000
+ days: true,
+ minutes: true,
+ hours: true,
+ seconds: true,
+ value: 10000000,
});
export const WithState = () => {
- const [v,s] = useState<number>(1000000)
- return <TestedComponent value={v} onChange={s} days minutes hours seconds />
-}
+ const [v, s] = useState<number>(1000000);
+ return <TestedComponent value={v} onChange={s} days minutes hours seconds />;
+};
diff --git a/packages/anastasis-webui/src/components/picker/DurationPicker.tsx b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
index 235a63e2d..8a1faf4d0 100644
--- a/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
+++ b/packages/anastasis-webui/src/components/picker/DurationPicker.tsx
@@ -15,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -30,75 +30,123 @@ export interface Props {
seconds?: boolean;
days?: boolean;
onChange: (value: number) => void;
- value: number
+ value: number;
}
// inspiration taken from https://github.com/flurmbo/react-duration-picker
-export function DurationPicker({ days, hours, minutes, seconds, onChange, value }: Props): VNode {
- const ss = 1000
- const ms = ss * 60
- const hs = ms * 60
- const ds = hs * 24
- const i18n = useTranslator()
-
- return <div class="rdp-picker">
- {days && <DurationColumn unit={i18n`days`} max={99}
- value={Math.floor(value / ds)}
- onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
- onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
- onChange={diff => onChange(value + diff * ds)}
- />}
- {hours && <DurationColumn unit={i18n`hours`} max={23} min={1}
- value={Math.floor(value / hs) % 24}
- onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
- onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
- onChange={diff => onChange(value + diff * hs)}
- />}
- {minutes && <DurationColumn unit={i18n`minutes`} max={59} min={1}
- value={Math.floor(value / ms) % 60}
- onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
- onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
- onChange={diff => onChange(value + diff * ms)}
- />}
- {seconds && <DurationColumn unit={i18n`seconds`} max={59}
- value={Math.floor(value / ss) % 60}
- onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
- onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
- onChange={diff => onChange(value + diff * ss)}
- />}
- </div>
+export function DurationPicker({
+ days,
+ hours,
+ minutes,
+ seconds,
+ onChange,
+ value,
+}: Props): VNode {
+ const ss = 1000;
+ const ms = ss * 60;
+ const hs = ms * 60;
+ const ds = hs * 24;
+ const i18n = useTranslator();
+
+ return (
+ <div class="rdp-picker">
+ {days && (
+ <DurationColumn
+ unit={i18n`days`}
+ max={99}
+ value={Math.floor(value / ds)}
+ onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
+ onChange={(diff) => onChange(value + diff * ds)}
+ />
+ )}
+ {hours && (
+ <DurationColumn
+ unit={i18n`hours`}
+ max={23}
+ min={1}
+ value={Math.floor(value / hs) % 24}
+ onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
+ onChange={(diff) => onChange(value + diff * hs)}
+ />
+ )}
+ {minutes && (
+ <DurationColumn
+ unit={i18n`minutes`}
+ max={59}
+ min={1}
+ value={Math.floor(value / ms) % 60}
+ onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
+ onChange={(diff) => onChange(value + diff * ms)}
+ />
+ )}
+ {seconds && (
+ <DurationColumn
+ unit={i18n`seconds`}
+ max={59}
+ value={Math.floor(value / ss) % 60}
+ onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
+ onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
+ onChange={(diff) => onChange(value + diff * ss)}
+ />
+ )}
+ </div>
+ );
}
interface ColProps {
- unit: string,
- min?: number,
- max: number,
- value: number,
+ unit: string;
+ min?: number;
+ max: number;
+ value: number;
onIncrease?: () => void;
onDecrease?: () => void;
onChange?: (diff: number) => void;
}
-function InputNumber({ initial, onChange }: { initial: number, onChange: (n: number) => void }) {
- const [value, handler] = useState<{v:string}>({
- v: toTwoDigitString(initial)
- })
-
- return <input
- value={value.v}
- onBlur={(e) => onChange(parseInt(value.v, 10))}
- onInput={(e) => {
- e.preventDefault()
- const n = Number.parseInt(e.currentTarget.value, 10);
- if (isNaN(n)) return handler({v:toTwoDigitString(initial)})
- return handler({v:toTwoDigitString(n)})
- }}
- style={{ width: 50, border: 'none', fontSize: 'inherit', background: 'inherit' }} />
-}
+function InputNumber({
+ initial,
+ onChange,
+}: {
+ initial: number;
+ onChange: (n: number) => void;
+}) {
+ const [value, handler] = useState<{ v: string }>({
+ v: toTwoDigitString(initial),
+ });
-function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onChange }: ColProps): VNode {
+ return (
+ <input
+ value={value.v}
+ onBlur={(e) => onChange(parseInt(value.v, 10))}
+ onInput={(e) => {
+ e.preventDefault();
+ const n = Number.parseInt(e.currentTarget.value, 10);
+ if (isNaN(n)) return handler({ v: toTwoDigitString(initial) });
+ return handler({ v: toTwoDigitString(n) });
+ }}
+ style={{
+ width: 50,
+ border: "none",
+ fontSize: "inherit",
+ background: "inherit",
+ }}
+ />
+ );
+}
- const cellHeight = 35
+function DurationColumn({
+ unit,
+ min = 0,
+ max,
+ value,
+ onIncrease,
+ onDecrease,
+ onChange,
+}: ColProps): VNode {
+ const cellHeight = 35;
return (
<div class="rdp-column-container">
<div class="rdp-masked-div">
@@ -106,49 +154,58 @@ function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onC
<hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} />
<div class="rdp-column" style={{ top: 0 }}>
-
<div class="rdp-cell" key={value - 2}>
- {onDecrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }}
- onClick={onDecrease}>
- <span class="icon">
- <i class="mdi mdi-chevron-up" />
- </span>
- </button>}
+ {onDecrease && (
+ <button
+ style={{ width: "100%", textAlign: "center", margin: 5 }}
+ onClick={onDecrease}
+ >
+ <span class="icon">
+ <i class="mdi mdi-chevron-up" />
+ </span>
+ </button>
+ )}
</div>
<div class="rdp-cell" key={value - 1}>
- {value > min ? toTwoDigitString(value - 1) : ''}
+ {value > min ? toTwoDigitString(value - 1) : ""}
</div>
<div class="rdp-cell rdp-center" key={value}>
- {onChange ?
- <InputNumber initial={value} onChange={(n) => onChange(n - value)} /> :
+ {onChange ? (
+ <InputNumber
+ initial={value}
+ onChange={(n) => onChange(n - value)}
+ />
+ ) : (
toTwoDigitString(value)
- }
+ )}
<div>{unit}</div>
</div>
<div class="rdp-cell" key={value + 1}>
- {value < max ? toTwoDigitString(value + 1) : ''}
+ {value < max ? toTwoDigitString(value + 1) : ""}
</div>
<div class="rdp-cell" key={value + 2}>
- {onIncrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }}
- onClick={onIncrease}>
- <span class="icon">
- <i class="mdi mdi-chevron-down" />
- </span>
- </button>}
+ {onIncrease && (
+ <button
+ style={{ width: "100%", textAlign: "center", margin: 5 }}
+ onClick={onIncrease}
+ >
+ <span class="icon">
+ <i class="mdi mdi-chevron-down" />
+ </span>
+ </button>
+ )}
</div>
-
</div>
</div>
</div>
);
}
-
function toTwoDigitString(n: number) {
if (n < 10) {
return `0${n}`;
}
return `${n}`;
-} \ No newline at end of file
+}
diff --git a/packages/anastasis-webui/src/context/anastasis.ts b/packages/anastasis-webui/src/context/anastasis.ts
index e7f93ed43..c2e7b2a47 100644
--- a/packages/anastasis-webui/src/context/anastasis.ts
+++ b/packages/anastasis-webui/src/context/anastasis.ts
@@ -15,19 +15,19 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createContext, h, VNode } from 'preact';
-import { useContext } from 'preact/hooks';
-import { AnastasisReducerApi } from '../hooks/use-anastasis-reducer';
+import { createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { AnastasisReducerApi } from "../hooks/use-anastasis-reducer";
type Type = AnastasisReducerApi | undefined;
-const initial = undefined
+const initial = undefined;
-const Context = createContext<Type>(initial)
+const Context = createContext<Type>(initial);
interface Props {
value: AnastasisReducerApi;
@@ -36,6 +36,6 @@ interface Props {
export const AnastasisProvider = ({ value, children }: Props): VNode => {
return h(Context.Provider, { value, children });
-}
+};
-export const useAnastasisContext = (): Type => useContext(Context); \ No newline at end of file
+export const useAnastasisContext = (): Type => useContext(Context);
diff --git a/packages/anastasis-webui/src/context/translation.ts b/packages/anastasis-webui/src/context/translation.ts
index 5ceb5d428..a47864d75 100644
--- a/packages/anastasis-webui/src/context/translation.ts
+++ b/packages/anastasis-webui/src/context/translation.ts
@@ -15,13 +15,13 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createContext, h, VNode } from 'preact'
-import { useContext, useEffect } from 'preact/hooks'
-import { useLang } from '../hooks'
+import { createContext, h, VNode } from "preact";
+import { useContext, useEffect } from "preact/hooks";
+import { useLang } from "../hooks";
import * as jedLib from "jed";
import { strings } from "../i18n/strings";
@@ -31,13 +31,13 @@ interface Type {
changeLanguage: (l: string) => void;
}
const initial = {
- lang: 'en',
+ lang: "en",
handler: null,
changeLanguage: () => {
// do not change anything
- }
-}
-const Context = createContext<Type>(initial)
+ },
+};
+const Context = createContext<Type>(initial);
interface Props {
initial?: string;
@@ -45,15 +45,22 @@ interface Props {
forceLang?: string;
}
-export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => {
- const [lang, changeLanguage] = useLang(initial)
+export const TranslationProvider = ({
+ initial,
+ children,
+ forceLang,
+}: Props): VNode => {
+ const [lang, changeLanguage] = useLang(initial);
useEffect(() => {
if (forceLang) {
- changeLanguage(forceLang)
+ changeLanguage(forceLang);
}
- })
- const handler = new jedLib.Jed(strings[lang] || strings['en']);
- return h(Context.Provider, { value: { lang, handler, changeLanguage }, children });
-}
+ });
+ const handler = new jedLib.Jed(strings[lang] || strings["en"]);
+ return h(Context.Provider, {
+ value: { lang, handler, changeLanguage },
+ children,
+ });
+};
-export const useTranslationContext = (): Type => useContext(Context); \ No newline at end of file
+export const useTranslationContext = (): Type => useContext(Context);
diff --git a/packages/anastasis-webui/src/declaration.d.ts b/packages/anastasis-webui/src/declaration.d.ts
index 2c4b7cb3a..00b3d41d5 100644
--- a/packages/anastasis-webui/src/declaration.d.ts
+++ b/packages/anastasis-webui/src/declaration.d.ts
@@ -1,20 +1,20 @@
declare module "*.css" {
- const mapping: Record<string, string>;
- export default mapping;
+ const mapping: Record<string, string>;
+ export default mapping;
}
-declare module '*.svg' {
- const content: any;
- export default content;
+declare module "*.svg" {
+ const content: any;
+ export default content;
}
-declare module '*.jpeg' {
- const content: any;
- export default content;
+declare module "*.jpeg" {
+ const content: any;
+ export default content;
}
-declare module '*.png' {
- const content: any;
- export default content;
+declare module "*.png" {
+ const content: any;
+ export default content;
}
-declare module 'jed' {
- const x: any;
- export = x;
+declare module "jed" {
+ const x: any;
+ export = x;
}
diff --git a/packages/anastasis-webui/src/hooks/async.ts b/packages/anastasis-webui/src/hooks/async.ts
index ea3ff6acf..0fc197554 100644
--- a/packages/anastasis-webui/src/hooks/async.ts
+++ b/packages/anastasis-webui/src/hooks/async.ts
@@ -15,9 +15,9 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { useState } from "preact/hooks";
// import { cancelPendingRequest } from "./backend";
@@ -34,36 +34,39 @@ export interface AsyncOperationApi<T> {
error: string | undefined;
}
-export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }): AsyncOperationApi<T> {
+export function useAsync<T>(
+ fn?: (...args: any) => Promise<T>,
+ { slowTolerance: tooLong }: Options = { slowTolerance: 1000 },
+): AsyncOperationApi<T> {
const [data, setData] = useState<T | undefined>(undefined);
const [isLoading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<any>(undefined);
- const [isSlow, setSlow] = useState(false)
+ const [isSlow, setSlow] = useState(false);
const request = async (...args: any) => {
if (!fn) return;
setLoading(true);
const handler = setTimeout(() => {
- setSlow(true)
- }, tooLong)
+ setSlow(true);
+ }, tooLong);
try {
- console.log("calling async", args)
+ console.log("calling async", args);
const result = await fn(...args);
- console.log("async back", result)
+ console.log("async back", result);
setData(result);
} catch (error) {
setError(error);
}
setLoading(false);
- setSlow(false)
- clearTimeout(handler)
+ setSlow(false);
+ clearTimeout(handler);
};
function cancel() {
// cancelPendingRequest()
setLoading(false);
- setSlow(false)
+ setSlow(false);
}
return {
@@ -72,6 +75,6 @@ export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance:
data,
isSlow,
isLoading,
- error
+ error,
};
}
diff --git a/packages/anastasis-webui/src/hooks/index.ts b/packages/anastasis-webui/src/hooks/index.ts
index 15df4f154..9a1b50a11 100644
--- a/packages/anastasis-webui/src/hooks/index.ts
+++ b/packages/anastasis-webui/src/hooks/index.ts
@@ -15,81 +15,110 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import { StateUpdater, useState } from "preact/hooks";
-export type ValueOrFunction<T> = T | ((p: T) => T)
-
+export type ValueOrFunction<T> = T | ((p: T) => T);
const calculateRootPath = () => {
- const rootPath = typeof window !== undefined ? window.location.origin + window.location.pathname : '/'
- return rootPath
-}
-
-export function useBackendURL(url?: string): [string, boolean, StateUpdater<string>, () => void] {
- const [value, setter] = useNotNullLocalStorage('backend-url', url || calculateRootPath())
- const [triedToLog, setTriedToLog] = useLocalStorage('tried-login')
+ const rootPath =
+ typeof window !== undefined
+ ? window.location.origin + window.location.pathname
+ : "/";
+ return rootPath;
+};
+
+export function useBackendURL(
+ url?: string,
+): [string, boolean, StateUpdater<string>, () => void] {
+ const [value, setter] = useNotNullLocalStorage(
+ "backend-url",
+ url || calculateRootPath(),
+ );
+ const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
const checkedSetter = (v: ValueOrFunction<string>) => {
- setTriedToLog('yes')
- return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, ''))
- }
+ setTriedToLog("yes");
+ return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
+ };
const resetBackend = () => {
- setTriedToLog(undefined)
- }
- return [value, !!triedToLog, checkedSetter, resetBackend]
+ setTriedToLog(undefined);
+ };
+ return [value, !!triedToLog, checkedSetter, resetBackend];
}
-export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] {
- return useLocalStorage('backend-token')
+export function useBackendDefaultToken(): [
+ string | undefined,
+ StateUpdater<string | undefined>,
+] {
+ return useLocalStorage("backend-token");
}
-export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>] {
- const [token, setToken] = useLocalStorage(`backend-token-${id}`)
- const [defaultToken, defaultSetToken] = useBackendDefaultToken()
+export function useBackendInstanceToken(
+ id: string,
+): [string | undefined, StateUpdater<string | undefined>] {
+ const [token, setToken] = useLocalStorage(`backend-token-${id}`);
+ const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token
- if (id === 'default') {
- return [defaultToken, defaultSetToken]
+ if (id === "default") {
+ return [defaultToken, defaultSetToken];
}
- return [token, setToken]
+ return [token, setToken];
}
export function useLang(initial?: string): [string, StateUpdater<string>] {
- const browserLang = typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : undefined;
- const defaultLang = (browserLang || initial || 'en').substring(0, 2)
- return useNotNullLocalStorage('lang-preference', defaultLang)
+ const browserLang =
+ typeof window !== "undefined"
+ ? navigator.language || (navigator as any).userLanguage
+ : undefined;
+ const defaultLang = (browserLang || initial || "en").substring(0, 2);
+ return useNotNullLocalStorage("lang-preference", defaultLang);
}
-export function useLocalStorage(key: string, initialValue?: string): [string | undefined, StateUpdater<string | undefined>] {
- const [storedValue, setStoredValue] = useState<string | undefined>((): string | undefined => {
- return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
+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
+ 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)
+ window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, toStore);
}
}
- return toStore
- })
+ return toStore;
+ });
};
return [storedValue, setValue];
}
-export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
+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;
+ return typeof window !== "undefined"
+ ? window.localStorage.getItem(key) || initialValue
+ : initialValue;
});
const setValue = (value: string | ((val: string) => string)) => {
@@ -97,7 +126,7 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
if (!valueToStore) {
- window.localStorage.removeItem(key)
+ window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, valueToStore);
}
@@ -106,5 +135,3 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
return [storedValue, setValue];
}
-
-
diff --git a/packages/anastasis-webui/src/i18n/index.tsx b/packages/anastasis-webui/src/i18n/index.tsx
index 63c8e1934..6e2c4e79a 100644
--- a/packages/anastasis-webui/src/i18n/index.tsx
+++ b/packages/anastasis-webui/src/i18n/index.tsx
@@ -27,23 +27,25 @@ import { useTranslationContext } from "../context/translation";
export function useTranslator() {
const ctx = useTranslationContext();
- const jed = ctx.handler
- return function str(stringSeq: TemplateStringsArray, ...values: any[]): string {
+ const jed = ctx.handler;
+ return function str(
+ stringSeq: TemplateStringsArray,
+ ...values: any[]
+ ): string {
const s = toI18nString(stringSeq);
- if (!s) return s
+ if (!s) return s;
const tr = jed
.translate(s)
.ifPlural(1, s)
.fetch(...values);
return tr;
- }
+ };
}
-
/**
* Convert template strings to a msgid
*/
- function toI18nString(stringSeq: ReadonlyArray<string>): string {
+function toI18nString(stringSeq: ReadonlyArray<string>): string {
let s = "";
for (let i = 0; i < stringSeq.length; i++) {
s += stringSeq[i];
@@ -54,7 +56,6 @@ export function useTranslator() {
return s;
}
-
interface TranslateSwitchProps {
target: number;
children: ComponentChildren;
@@ -110,7 +111,7 @@ function getTranslatedChildren(
// Text
result.push(tr[i]);
} else {
- const childIdx = Number.parseInt(tr[i],10) - 1;
+ const childIdx = Number.parseInt(tr[i], 10) - 1;
result.push(placeholderChildren[childIdx]);
}
}
@@ -131,9 +132,9 @@ function getTranslatedChildren(
*/
export function Translate({ children }: TranslateProps): VNode {
const s = stringifyChildren(children);
- const ctx = useTranslationContext()
+ const ctx = useTranslationContext();
const translation: string = ctx.handler.ngettext(s, s, 1);
- const result = getTranslatedChildren(translation, children)
+ const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
}
@@ -154,14 +155,16 @@ export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
let plural: VNode<TranslationPluralProps> | undefined;
// const children = this.props.children;
if (children) {
- (children instanceof Array ? children : [children]).forEach((child: any) => {
- if (child.type === TranslatePlural) {
- plural = child;
- }
- if (child.type === TranslateSingular) {
- singular = child;
- }
- });
+ (children instanceof Array ? children : [children]).forEach(
+ (child: any) => {
+ if (child.type === TranslatePlural) {
+ plural = child;
+ }
+ if (child.type === TranslateSingular) {
+ singular = child;
+ }
+ },
+ );
}
if (!singular || !plural) {
console.error("translation not found");
@@ -182,9 +185,12 @@ interface TranslationPluralProps {
/**
* See [[TranslateSwitch]].
*/
-export function TranslatePlural({ children, target }: TranslationPluralProps): VNode {
+export function TranslatePlural({
+ children,
+ target,
+}: TranslationPluralProps): VNode {
const s = stringifyChildren(children);
- const ctx = useTranslationContext()
+ const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, 1);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
@@ -193,11 +199,13 @@ export function TranslatePlural({ children, target }: TranslationPluralProps): V
/**
* See [[TranslateSwitch]].
*/
-export function TranslateSingular({ children, target }: TranslationPluralProps): VNode {
+export function TranslateSingular({
+ children,
+ target,
+}: TranslationPluralProps): VNode {
const s = stringifyChildren(children);
- const ctx = useTranslationContext()
+ const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, target);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
-
}
diff --git a/packages/anastasis-webui/src/i18n/strings.ts b/packages/anastasis-webui/src/i18n/strings.ts
index b4f376ce0..d12e63e88 100644
--- a/packages/anastasis-webui/src/i18n/strings.ts
+++ b/packages/anastasis-webui/src/i18n/strings.ts
@@ -15,30 +15,30 @@
*/
/*eslint quote-props: ["error", "consistent"]*/
-export const strings: {[s: string]: any} = {};
+export const strings: { [s: string]: any } = {};
-strings['de'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
+strings["de"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
"": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
},
- }
- }
+ },
+ },
};
-strings['en'] = {
- "domain": "messages",
- "locale_data": {
- "messages": {
+strings["en"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
"": {
- "domain": "messages",
- "plural_forms": "nplurals=2; plural=(n != 1);",
- "lang": ""
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
},
- }
- }
+ },
+ },
};
diff --git a/packages/anastasis-webui/src/index.ts b/packages/anastasis-webui/src/index.ts
index e78b9c194..4bd7b28f3 100644
--- a/packages/anastasis-webui/src/index.ts
+++ b/packages/anastasis-webui/src/index.ts
@@ -1,4 +1,4 @@
-import App from './components/app';
-import './scss/main.scss';
+import App from "./components/app";
+import "./scss/main.scss";
export default App;
diff --git a/packages/anastasis-webui/src/manifest.json b/packages/anastasis-webui/src/manifest.json
index 6b44a2b31..2752dad77 100644
--- a/packages/anastasis-webui/src/manifest.json
+++ b/packages/anastasis-webui/src/manifest.json
@@ -18,4 +18,4 @@
"sizes": "512x512"
}
]
-} \ No newline at end of file
+}
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
index 43807fefe..9b067127d 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,24 +15,23 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { AddingProviderScreen as TestedComponent } from './AddingProviderScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { AddingProviderScreen as TestedComponent } from "./AddingProviderScreen";
export default {
- title: 'Pages/backup/AddingProviderScreen',
+ title: "Pages/ManageProvider",
component: TestedComponent,
args: {
- order: 4,
+ order: 1,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
@@ -41,10 +39,31 @@ export const NewProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
} as ReducerState);
-export const NewSMSProvider = createExample(TestedComponent, {
+export const NewProviderWithoutProviderList = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
-} as ReducerState, { providerType: 'sms'});
+ authentication_providers: {},
+} as ReducerState);
-export const NewIBANProvider = createExample(TestedComponent, {
- ...reducerStatesExample.authEditing,
-} as ReducerState, { providerType: 'iban' });
+export const NewVideoProvider = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.authEditing,
+ } as ReducerState,
+ { providerType: "video" },
+);
+
+export const NewSmsProvider = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.authEditing,
+ } as ReducerState,
+ { providerType: "sms" },
+);
+
+export const NewIBANProvider = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.authEditing,
+ } as ReducerState,
+ { providerType: "iban" },
+);
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
index 9c83da49e..96b38e92d 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
@@ -1,101 +1,260 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
+import { AuthenticationProviderStatusOk } from "anastasis-core";
import { h, VNode } from "preact";
-import { useLayoutEffect, useRef, useState } from "preact/hooks";
+import { useEffect, useRef, useState } from "preact/hooks";
import { TextInput } from "../../components/fields/TextInput";
+import { useAnastasisContext } from "../../context/anastasis";
import { authMethods, KnownAuthMethods } from "./authMethod";
import { AnastasisClientFrame } from "./index";
interface Props {
providerType?: KnownAuthMethods;
- cancel: () => void;
+ onCancel: () => void;
}
-export function AddingProviderScreen({ providerType, cancel }: Props): VNode {
+
+async function testProvider(
+ url: string,
+ expectedMethodType?: string,
+): Promise<void> {
+ try {
+ const response = await fetch(new URL("config", url).href);
+ const json = await response.json().catch((d) => ({}));
+ if (!("methods" in json) || !Array.isArray(json.methods)) {
+ throw Error(
+ "This provider doesn't have authentication method. Check the provider URL",
+ );
+ }
+ console.log("expected", expectedMethodType);
+ if (!expectedMethodType) {
+ return;
+ }
+ let found = false;
+ for (let i = 0; i < json.methods.length && !found; i++) {
+ found = json.methods[i].type === expectedMethodType;
+ }
+ if (!found) {
+ throw Error(
+ `This provider does not support authentication method ${expectedMethodType}`,
+ );
+ }
+ return;
+ } catch (e) {
+ console.log("error", e);
+ const error =
+ e instanceof Error
+ ? Error(
+ `There was an error testing this provider, try another one. ${e.message}`,
+ )
+ : Error(`There was an error testing this provider, try another one.`);
+ throw error;
+ }
+}
+
+export function AddingProviderScreen({ providerType, onCancel }: Props): VNode {
+ const reducer = useAnastasisContext();
+
const [providerURL, setProviderURL] = useState("");
- const [error, setError] = useState<string | undefined>()
- const providerLabel = providerType ? authMethods[providerType].label : undefined
+ const [error, setError] = useState<string | undefined>();
+ const [testing, setTesting] = useState(false);
+ const providerLabel = providerType
+ ? authMethods[providerType].label
+ : undefined;
- function testProvider(): void {
- setError(undefined)
+ //FIXME: move this timeout logic into a hook
+ const timeout = useRef<number | undefined>(undefined);
+ useEffect(() => {
+ if (timeout) window.clearTimeout(timeout.current);
+ timeout.current = window.setTimeout(async () => {
+ const url = providerURL.endsWith("/") ? providerURL : providerURL + "/";
+ if (!providerURL || authProviders.includes(url)) return;
+ try {
+ setTesting(true);
+ await testProvider(url, providerType);
+ // this is use as tested but everything when ok
+ // undefined will mean that the field is not dirty
+ setError("");
+ } catch (e) {
+ console.log("tuvieja", e);
+ if (e instanceof Error) setError(e.message);
+ }
+ setTesting(false);
+ }, 200);
+ }, [providerURL, reducer]);
- fetch(`${providerURL}/config`)
- .then(r => r.json().catch(d => ({})))
- .then(r => {
- if (!("methods" in r) || !Array.isArray(r.methods)) {
- setError("This provider doesn't have authentication method. Check the provider URL")
- return;
- }
- if (!providerLabel) {
- setError("")
- return
- }
- let found = false
- for (let i = 0; i < r.methods.length && !found; i++) {
- found = r.methods[i].type !== providerType
- }
- if (!found) {
- setError(`This provider does not support authentication method ${providerLabel}`)
- }
- })
- .catch(e => {
- setError(`There was an error testing this provider, try another one. ${e.message}`)
- })
+ if (!reducer) {
+ return <div>no reducer in context</div>;
+ }
+ if (
+ !reducer.currentReducerState ||
+ !("authentication_providers" in reducer.currentReducerState)
+ ) {
+ return <div>invalid state</div>;
}
- function addProvider(): void {
- // addAuthMethod({
- // authentication_method: {
- // type: "sms",
- // instructions: `SMS to ${providerURL}`,
- // challenge: encodeCrock(stringToBytes(providerURL)),
- // },
- // });
+
+ async function addProvider(provider_url: string): Promise<void> {
+ await reducer?.transition("add_provider", { provider_url });
+ onCancel();
+ }
+ function deleteProvider(provider_url: string): void {
+ reducer?.transition("delete_provider", { provider_url });
}
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- inputRef.current?.focus();
- }, []);
- let errors = !providerURL ? 'Add provider URL' : undefined
+ const allAuthProviders =
+ reducer.currentReducerState.authentication_providers || {};
+ const authProviders = Object.keys(allAuthProviders).filter((provUrl) => {
+ const p = allAuthProviders[provUrl];
+ if (!providerLabel) {
+ return p && "currency" in p;
+ } else {
+ return (
+ p &&
+ "currency" in p &&
+ p.methods.findIndex((m) => m.type === providerType) !== -1
+ );
+ }
+ });
+
+ let errors = !providerURL ? "Add provider URL" : undefined;
+ let url: string | undefined;
try {
- new URL(providerURL)
+ url = new URL("", providerURL).href;
} catch {
- errors = 'Check the URL'
+ errors = "Check the URL";
}
if (!!error && !errors) {
- errors = error
+ errors = error;
+ }
+ if (!errors && authProviders.includes(url!)) {
+ errors = "That provider is already known";
}
return (
- <AnastasisClientFrame hideNav
- title={!providerLabel ? `Backup: Adding a provider` : `Backup: Adding a ${providerLabel} provider`}
- hideNext={errors}>
+ <AnastasisClientFrame
+ hideNav
+ title="Backup: Manage providers"
+ hideNext={errors}
+ >
<div>
- <p>
- Add a provider url {errors}
- </p>
+ {!providerLabel ? (
+ <p>Add a provider url</p>
+ ) : (
+ <p>Add a provider url for a {providerLabel} service</p>
+ )}
<div class="container">
<TextInput
label="Provider URL"
placeholder="https://provider.com"
grabFocus
- bind={[providerURL, setProviderURL]} />
- </div>
- {!!error && <p class="block has-text-danger">{error}</p>}
- {error === "" && <p class="block has-text-success">This provider worked!</p>}
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={testProvider}>TEST</button>
+ error={errors}
+ bind={[providerURL, setProviderURL]}
+ />
</div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <p class="block">Example: https://kudos.demo.anastasis.lu</p>
+ {testing && <p class="has-text-info">Testing</p>}
+
+ <div
+ class="block"
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addProvider}>Add</button>
+ <button
+ class="button is-info"
+ disabled={error !== "" || testing}
+ onClick={() => addProvider(url!)}
+ >
+ Add
+ </button>
</span>
</div>
+
+ {authProviders.length > 0 ? (
+ !providerLabel ? (
+ <p class="subtitle">Current providers</p>
+ ) : (
+ <p class="subtitle">
+ Current providers for {providerLabel} service
+ </p>
+ )
+ ) : !providerLabel ? (
+ <p class="subtitle">No known providers, add one.</p>
+ ) : (
+ <p class="subtitle">No known providers for {providerLabel} service</p>
+ )}
+
+ {authProviders.map((k) => {
+ const p = allAuthProviders[k] as AuthenticationProviderStatusOk;
+ return <TableRow url={k} info={p} onDelete={deleteProvider} />;
+ })}
</div>
</AnastasisClientFrame>
);
}
+function TableRow({
+ url,
+ info,
+ onDelete,
+}: {
+ onDelete: (s: string) => void;
+ url: string;
+ info: AuthenticationProviderStatusOk;
+}) {
+ const [status, setStatus] = useState("checking");
+ useEffect(function () {
+ testProvider(url.endsWith("/") ? url.substring(0, url.length - 1) : url)
+ .then(function () {
+ setStatus("responding");
+ })
+ .catch(function () {
+ setStatus("failed to contact");
+ });
+ });
+ return (
+ <div
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <div>
+ <div class="subtitle">{url}</div>
+ <dl>
+ <dt>
+ <b>Business Name</b>
+ </dt>
+ <dd>{info.business_name}</dd>
+ <dt>
+ <b>Supported methods</b>
+ </dt>
+ <dd>{info.methods.map((m) => m.type).join(",")}</dd>
+ <dt>
+ <b>Maximum storage</b>
+ </dt>
+ <dd>{info.storage_limit_in_megabytes} Mb</dd>
+ <dt>
+ <b>Status</b>
+ </dt>
+ <dd>{status}</dd>
+ </dl>
+ </div>
+ <div
+ class="block"
+ style={{
+ marginTop: "auto",
+ marginBottom: "auto",
+ display: "flex",
+ justifyContent: "space-between",
+ flexDirection: "column",
+ }}
+ >
+ <button class="button is-danger" onClick={() => onDelete(url)}>
+ Remove
+ </button>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
index 549686616..d48e94403 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,76 +15,83 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { AttributeEntryScreen as TestedComponent } from './AttributeEntryScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { AttributeEntryScreen as TestedComponent } from "./AttributeEntryScreen";
export default {
- title: 'Pages/AttributeEntryScreen',
+ title: "Pages/PersonalInformation",
component: TestedComponent,
args: {
- order: 4,
+ order: 3,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
export const Backup = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing,
- required_attributes: [{
- name: 'first name',
- label: 'first',
- type: 'string',
- uuid: 'asdasdsa1',
- widget: 'wid',
- }, {
- name: 'last name',
- label: 'second',
- type: 'string',
- uuid: 'asdasdsa2',
- widget: 'wid',
- }, {
- name: 'birthdate',
- label: 'birthdate',
- type: 'date',
- uuid: 'asdasdsa3',
- widget: 'calendar',
- }]
+ required_attributes: [
+ {
+ name: "first name",
+ label: "first",
+ type: "string",
+ uuid: "asdasdsa1",
+ widget: "wid",
+ },
+ {
+ name: "last name",
+ label: "second",
+ type: "string",
+ uuid: "asdasdsa2",
+ widget: "wid",
+ },
+ {
+ name: "birthdate",
+ label: "birthdate",
+ type: "date",
+ uuid: "asdasdsa3",
+ widget: "calendar",
+ },
+ ],
} as ReducerState);
export const Recovery = createExample(TestedComponent, {
...reducerStatesExample.recoveryAttributeEditing,
- required_attributes: [{
- name: 'first',
- label: 'first',
- type: 'string',
- uuid: 'asdasdsa1',
- widget: 'wid',
- }, {
- name: 'pepe',
- label: 'second',
- type: 'string',
- uuid: 'asdasdsa2',
- widget: 'wid',
- }, {
- name: 'pepe2',
- label: 'third',
- type: 'date',
- uuid: 'asdasdsa3',
- widget: 'calendar',
- }]
+ required_attributes: [
+ {
+ name: "first",
+ label: "first",
+ type: "string",
+ uuid: "asdasdsa1",
+ widget: "wid",
+ },
+ {
+ name: "pepe",
+ label: "second",
+ type: "string",
+ uuid: "asdasdsa2",
+ widget: "wid",
+ },
+ {
+ name: "pepe2",
+ label: "third",
+ type: "date",
+ uuid: "asdasdsa3",
+ widget: "calendar",
+ },
+ ],
} as ReducerState);
export const WithNoRequiredAttribute = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing,
- required_attributes: undefined
+ required_attributes: undefined,
} as ReducerState);
const allWidgets = [
@@ -108,23 +114,22 @@ const allWidgets = [
"anastasis_gtk_ia_tax_de",
"anastasis_gtk_xx_prime",
"anastasis_gtk_xx_square",
-]
+];
function typeForWidget(name: string): string {
- if (["anastasis_gtk_xx_prime",
- "anastasis_gtk_xx_square",
- ].includes(name)) return "number";
- if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date"
+ if (["anastasis_gtk_xx_prime", "anastasis_gtk_xx_square"].includes(name))
+ return "number";
+ if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date";
return "string";
}
export const WithAllPosibleWidget = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing,
- required_attributes: allWidgets.map(w => ({
+ required_attributes: allWidgets.map((w) => ({
name: w,
label: `widget: ${w}`,
type: typeForWidget(w),
uuid: `uuid-${w}`,
- widget: w
- }))
+ widget: w,
+ })),
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
index f86994c97..1b50779e0 100644
--- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx
@@ -1,33 +1,42 @@
-/* eslint-disable @typescript-eslint/camelcase */
import { UserAttributeSpec, validators } from "anastasis-core";
-import { Fragment, h, VNode } from "preact";
+import { isAfter, parse } from "date-fns";
+import { h, VNode } from "preact";
import { useState } from "preact/hooks";
+import { DateInput } from "../../components/fields/DateInput";
+import { PhoneNumberInput } from "../../components/fields/NumberInput";
+import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis";
+import { ConfirmModal } from "./ConfirmModal";
import { AnastasisClientFrame, withProcessLabel } from "./index";
-import { TextInput } from "../../components/fields/TextInput";
-import { DateInput } from "../../components/fields/DateInput";
-import { NumberInput } from "../../components/fields/NumberInput";
-import { isAfter, parse } from "date-fns";
export function AttributeEntryScreen(): VNode {
- const reducer = useAnastasisContext()
- const state = reducer?.currentReducerState
- const currentIdentityAttributes = state && "identity_attributes" in state ? (state.identity_attributes || {}) : {}
- const [attrs, setAttrs] = useState<Record<string, string>>(currentIdentityAttributes);
+ const reducer = useAnastasisContext();
+ const state = reducer?.currentReducerState;
+ const currentIdentityAttributes =
+ state && "identity_attributes" in state
+ ? state.identity_attributes || {}
+ : {};
+ const [attrs, setAttrs] = useState<Record<string, string>>(
+ currentIdentityAttributes,
+ );
+ const [askUserIfSure, setAskUserIfSure] = useState(false);
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || !("required_attributes" in reducer.currentReducerState)) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ !("required_attributes" in reducer.currentReducerState)
+ ) {
+ return <div>invalid state</div>;
}
- const reqAttr = reducer.currentReducerState.required_attributes || []
+ const reqAttr = reducer.currentReducerState.required_attributes || [];
let hasErrors = false;
const fieldList: VNode[] = reqAttr.map((spec, i: number) => {
- const value = attrs[spec.name]
- const error = checkIfValid(value, spec)
- hasErrors = hasErrors || error !== undefined
+ const value = attrs[spec.name];
+ const error = checkIfValid(value, spec);
+ hasErrors = hasErrors || error !== undefined;
return (
<AttributeEntryField
key={i}
@@ -35,23 +44,42 @@ export function AttributeEntryScreen(): VNode {
setValue={(v: string) => setAttrs({ ...attrs, [spec.name]: v })}
spec={spec}
errorMessage={error}
- value={value} />
+ onConfirm={() => {
+ if (!hasErrors) {
+ setAskUserIfSure(true)
+ }
+ }}
+ value={value}
+ />
);
- })
+ });
return (
<AnastasisClientFrame
title={withProcessLabel(reducer, "Who are you?")}
hideNext={hasErrors ? "Complete the form." : undefined}
- onNext={() => reducer.transition("enter_user_attributes", {
- identity_attributes: attrs,
- })}
+ onNext={async () => setAskUserIfSure(true) }
>
- <div class="columns" style={{ maxWidth: 'unset' }}>
- <div class="column is-half">
- {fieldList}
- </div>
- <div class="column is-is-half" >
+ {askUserIfSure ? (
+ <ConfirmModal
+ active
+ onCancel={() => setAskUserIfSure(false)}
+ description="The values in the form must be correct"
+ label="I am sure"
+ cancelLabel="Wait, I want to check"
+ onConfirm={() => reducer.transition("enter_user_attributes", {
+ identity_attributes: attrs,
+ }).then(() => setAskUserIfSure(false) )}
+ >
+ You personal information is used to define the location where your
+ secret will be safely stored. If you forget what you have entered or
+ if there is a misspell you will be unable to recover your secret.
+ </ConfirmModal>
+ ) : null}
+
+ <div class="columns" style={{ maxWidth: "unset" }}>
+ <div class="column">{fieldList}</div>
+ <div class="column">
<p>This personal information will help to locate your secret.</p>
<h1 class="title">This stays private</h1>
<p>The information you have entered here:</p>
@@ -62,9 +90,12 @@ export function AttributeEntryScreen(): VNode {
</span>
Will be hashed, and therefore unreadable
</li>
- <li><span class="icon is-right">
- <i class="mdi mdi-circle-small" />
- </span>The non-hashed version is not shared</li>
+ <li>
+ <span class="icon is-right">
+ <i class="mdi mdi-circle-small" />
+ </span>
+ The non-hashed version is not shared
+ </li>
</ul>
</div>
</div>
@@ -78,39 +109,43 @@ interface AttributeEntryFieldProps {
setValue: (newValue: string) => void;
spec: UserAttributeSpec;
errorMessage: string | undefined;
+ onConfirm: () => void;
}
-const possibleBirthdayYear: Array<number> = []
+const possibleBirthdayYear: Array<number> = [];
for (let i = 0; i < 100; i++) {
- possibleBirthdayYear.push(2020 - i)
+ possibleBirthdayYear.push(2020 - i);
}
function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
-
return (
<div>
- {props.spec.type === 'date' &&
+ {props.spec.type === "date" &&
<DateInput
grabFocus={props.isFirst}
label={props.spec.label}
years={possibleBirthdayYear}
+ onConfirm={props.onConfirm}
error={props.errorMessage}
bind={[props.value, props.setValue]}
- />}
+ />
+ }
{props.spec.type === 'number' &&
- <NumberInput
+ <PhoneNumberInput
grabFocus={props.isFirst}
label={props.spec.label}
+ onConfirm={props.onConfirm}
error={props.errorMessage}
bind={[props.value, props.setValue]}
/>
}
- {props.spec.type === 'string' &&
+ {props.spec.type === "string" && (
<TextInput
grabFocus={props.isFirst}
label={props.spec.label}
+ onConfirm={props.onConfirm}
error={props.errorMessage}
bind={[props.value, props.setValue]}
/>
- }
+ )}
<div class="block">
This stays private
<span class="icon is-right">
@@ -120,40 +155,43 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
</div>
);
}
-const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/
-
+const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/;
-function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined {
- const pattern = spec['validation-regex']
+function checkIfValid(
+ value: string,
+ spec: UserAttributeSpec,
+): string | undefined {
+ const pattern = spec["validation-regex"];
if (pattern) {
- const re = new RegExp(pattern)
- if (!re.test(value)) return 'The value is invalid'
+ const re = new RegExp(pattern);
+ if (!re.test(value)) return "The value is invalid";
}
- const logic = spec['validation-logic']
+ const logic = spec["validation-logic"];
if (logic) {
const func = (validators as any)[logic];
- if (func && typeof func === 'function' && !func(value)) return 'Please check the value'
+ if (func && typeof func === "function" && !func(value))
+ return "Please check the value";
}
- const optional = spec.optional
+ const optional = spec.optional;
if (!optional && !value) {
- return 'This value is required'
+ return "This value is required";
}
if ("date" === spec.type) {
if (!YEAR_REGEX.test(value)) {
- return "The date doesn't follow the format"
+ return "The date doesn't follow the format";
}
try {
- const v = parse(value, 'yyyy-MM-dd', new Date());
+ const v = parse(value, "yyyy-MM-dd", new Date());
if (Number.isNaN(v.getTime())) {
- return "Some numeric values seems out of range for a date"
+ return "Some numeric values seems out of range for a date";
}
if ("birthdate" === spec.name && isAfter(v, new Date())) {
- return "A birthdate cannot be in the future"
+ return "A birthdate cannot be in the future";
}
} catch (e) {
- return "Could not parse the date"
+ return "Could not parse the date";
}
}
- return undefined
+ return undefined;
}
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
index 5077c3eb0..8acf1c8c8 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,78 +15,84 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationEditorScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { AuthenticationEditorScreen as TestedComponent } from "./AuthenticationEditorScreen";
export default {
- title: 'Pages/backup/AuthenticationEditorScreen',
+ title: "Pages/backup/AuthorizationMethod",
component: TestedComponent,
args: {
- order: 5,
+ order: 4,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Example = createExample(TestedComponent, reducerStatesExample.authEditing);
+export const InitialState = createExample(
+ TestedComponent,
+ reducerStatesExample.authEditing,
+);
export const OneAuthMethodConfigured = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
- authentication_methods: [{
- type: 'question',
- instructions: 'what time is it?',
- challenge: 'asd',
- }]
+ authentication_methods: [
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ ],
} as ReducerState);
-
export const SomeMoreAuthMethodConfigured = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
- authentication_methods: [{
- type: 'question',
- instructions: 'what time is it?',
- challenge: 'asd',
- },{
- type: 'question',
- instructions: 'what time is it?',
- challenge: 'qwe',
- },{
- type: 'sms',
- instructions: 'what time is it?',
- challenge: 'asd',
- },{
- type: 'email',
- instructions: 'what time is it?',
- challenge: 'asd',
- },{
- type: 'email',
- instructions: 'what time is it?',
- challenge: 'asd',
- },{
- type: 'email',
- instructions: 'what time is it?',
- challenge: 'asd',
- },{
- type: 'email',
- instructions: 'what time is it?',
- challenge: 'asd',
- }]
+ authentication_methods: [
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "question",
+ instructions: "what time is it?",
+ challenge: "qwe",
+ },
+ {
+ type: "sms",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ {
+ type: "email",
+ instructions: "what time is it?",
+ challenge: "asd",
+ },
+ ],
} as ReducerState);
export const NoAuthMethodProvided = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
authentication_providers: {},
- authentication_methods: []
+ authentication_methods: [],
} as ReducerState);
-
- // type: string;
- // instructions: string;
- // challenge: string;
- // mime_type?: string;
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
index 93ca81194..91195971d 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
@@ -1,61 +1,85 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import { AuthMethod } from "anastasis-core";
+import { AuthMethod, ReducerStateBackup } from "anastasis-core";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis";
-import { authMethods, KnownAuthMethods } from "./authMethod";
+import { AddingProviderScreen } from "./AddingProviderScreen";
+import {
+ authMethods,
+ AuthMethodSetupProps,
+ AuthMethodWithRemove,
+ isKnownAuthMethods,
+ KnownAuthMethods,
+} from "./authMethod";
+import { ConfirmModal } from "./ConfirmModal";
import { AnastasisClientFrame } from "./index";
-
-
-const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>
+const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;
export function AuthenticationEditorScreen(): VNode {
- const [noProvidersAck, setNoProvidersAck] = useState(false)
- const [selectedMethod, setSelectedMethod] = useState<KnownAuthMethods | undefined>(undefined);
- const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined)
+ const [noProvidersAck, setNoProvidersAck] = useState(false);
+ const [selectedMethod, setSelectedMethod] = useState<
+ KnownAuthMethods | undefined
+ >(undefined);
+ const [tooFewAuths, setTooFewAuths] = useState(false);
+ const [manageProvider, setManageProvider] = useState<string | undefined>(
+ undefined,
+ );
- const reducer = useAnastasisContext()
+ // const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined)
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const configuredAuthMethods: AuthMethod[] = reducer.currentReducerState.authentication_methods ?? [];
- const haveMethodsConfigured = configuredAuthMethods.length > 0;
+ const configuredAuthMethods: AuthMethod[] =
+ reducer.currentReducerState.authentication_methods ?? [];
function removeByIndex(index: number): void {
- if (reducer) reducer.transition("delete_authentication", {
- authentication_method: index,
- })
+ if (reducer)
+ reducer.transition("delete_authentication", {
+ authentication_method: index,
+ });
}
- const camByType: { [s: string]: AuthMethodWithRemove[] } = {}
+ const camByType: { [s: string]: AuthMethodWithRemove[] } = {};
for (let index = 0; index < configuredAuthMethods.length; index++) {
const cam = {
...configuredAuthMethods[index],
- remove: () => removeByIndex(index)
- }
- const prevValue = camByType[cam.type] || []
- prevValue.push(cam)
+ remove: () => removeByIndex(index),
+ };
+ const prevValue = camByType[cam.type] || [];
+ prevValue.push(cam);
camByType[cam.type] = prevValue;
}
-
const providers = reducer.currentReducerState.authentication_providers!;
const authAvailableSet = new Set<string>();
for (const provKey of Object.keys(providers)) {
const p = providers[provKey];
- if ("http_status" in p && (!("error_code" in p)) && p.methods) {
+ if ("http_status" in p && !("error_code" in p) && p.methods) {
for (const meth of p.methods) {
authAvailableSet.add(meth.type);
}
}
}
+ if (manageProvider !== undefined) {
+ return (
+ <AddingProviderScreen
+ onCancel={() => setManageProvider(undefined)}
+ providerType={
+ isKnownAuthMethods(manageProvider) ? manageProvider : undefined
+ }
+ />
+ );
+ }
+
if (selectedMethod) {
const cancel = (): void => setSelectedMethod(undefined);
const addMethod = (args: any): void => {
@@ -63,120 +87,154 @@ export function AuthenticationEditorScreen(): VNode {
setSelectedMethod(undefined);
};
- const AuthSetup = authMethods[selectedMethod].screen ?? AuthMethodNotImplemented;
- return (<Fragment>
- <AuthSetup
- cancel={cancel}
- configured={camByType[selectedMethod] || []}
- addAuthMethod={addMethod}
- method={selectedMethod} />
-
- {!authAvailableSet.has(selectedMethod) && <ConfirmModal active
- onCancel={cancel} description="No providers founds" label="Add a provider manually"
- onConfirm={() => {
- null
- }}
- >
- We have found no trusted cloud providers for your recovery secret. You can add a provider manually.
- To add a provider you must know the provider URL (e.g. https://provider.com)
- <p>
- <a>More about cloud providers</a>
- </p>
- </ConfirmModal>}
-
- </Fragment>
+ const AuthSetup =
+ authMethods[selectedMethod].setup ?? AuthMethodNotImplemented;
+ return (
+ <Fragment>
+ <AuthSetup
+ cancel={cancel}
+ configured={camByType[selectedMethod] || []}
+ addAuthMethod={addMethod}
+ method={selectedMethod}
+ />
+
+ {!authAvailableSet.has(selectedMethod) && (
+ <ConfirmModal
+ active
+ onCancel={cancel}
+ description="No providers founds"
+ label="Add a provider manually"
+ onConfirm={async () => {
+ setManageProvider(selectedMethod);
+ }}
+ >
+ <p>
+ We have found no Anastasis providers that support this
+ authentication method. You can add a provider manually. To add a
+ provider you must know the provider URL (e.g.
+ https://provider.com)
+ </p>
+ <p>
+ <a>Learn more about Anastasis providers</a>
+ </p>
+ </ConfirmModal>
+ )}
+ </Fragment>
);
}
- if (addingProvider !== undefined) {
- return <div />
- }
-
function MethodButton(props: { method: KnownAuthMethods }): VNode {
- if (authMethods[props.method].skip) return <div />
-
+ if (authMethods[props.method].skip) return <div />;
+
return (
<div class="block">
<button
- style={{ justifyContent: 'space-between' }}
+ style={{ justifyContent: "space-between" }}
class="button is-fullwidth"
onClick={() => {
setSelectedMethod(props.method);
}}
>
- <div style={{ display: 'flex' }}>
- <span class="icon ">
- {authMethods[props.method].icon}
- </span>
- {authAvailableSet.has(props.method) ?
- <span>
- Add a {authMethods[props.method].label} challenge
- </span> :
- <span>
- Add a {authMethods[props.method].label} provider
- </span>
- }
+ <div style={{ display: "flex" }}>
+ <span class="icon ">{authMethods[props.method].icon}</span>
+ {authAvailableSet.has(props.method) ? (
+ <span>Add a {authMethods[props.method].label} challenge</span>
+ ) : (
+ <span>Add a {authMethods[props.method].label} provider</span>
+ )}
</div>
- {!authAvailableSet.has(props.method) &&
- <span class="icon has-text-danger" >
+ {!authAvailableSet.has(props.method) && (
+ <span class="icon has-text-danger">
<i class="mdi mdi-exclamation-thick" />
</span>
- }
- {camByType[props.method] &&
- <span class="tag is-info" >
- {camByType[props.method].length}
- </span>
- }
+ )}
+ {camByType[props.method] && (
+ <span class="tag is-info">{camByType[props.method].length}</span>
+ )}
</button>
</div>
);
}
- const errors = !haveMethodsConfigured ? "There is not enough authentication methods." : undefined;
+ const errors = configuredAuthMethods.length < 2 ? "There is not enough authentication methods." : undefined;
+ const handleNext = async () => {
+ const st = reducer.currentReducerState as ReducerStateBackup;
+ if ((st.authentication_methods ?? []).length <= 2) {
+ setTooFewAuths(true);
+ } else {
+ await reducer.transition("next", {});
+ }
+ };
return (
- <AnastasisClientFrame title="Backup: Configure Authentication Methods" hideNext={errors}>
+ <AnastasisClientFrame
+ title="Backup: Configure Authentication Methods"
+ hideNext={errors}
+ onNext={handleNext}
+ >
<div class="columns">
- <div class="column is-half">
+ <div class="column">
<div>
- {getKeys(authMethods).map(method => <MethodButton key={method} method={method} />)}
+ {getKeys(authMethods).map((method) => (
+ <MethodButton key={method} method={method} />
+ ))}
</div>
- {authAvailableSet.size === 0 && <ConfirmModal active={!noProvidersAck}
- onCancel={() => setNoProvidersAck(true)} description="No providers founds" label="Add a provider manually"
- onConfirm={() => {
- null
- }}
- >
- We have found no trusted cloud providers for your recovery secret. You can add a provider manually.
- To add a provider you must know the provider URL (e.g. https://provider.com)
- <p>
- <a>More about cloud providers</a>
- </p>
- </ConfirmModal>}
+ {tooFewAuths ? (
+ <ConfirmModal
+ active={tooFewAuths}
+ onCancel={() => setTooFewAuths(false)}
+ description="Too few auth methods configured"
+ label="Proceed anyway"
+ onConfirm={() => reducer.transition("next", {})}
+ >
+ You have selected fewer than 3 authentication methods. We
+ recommend that you add at least 3.
+ </ConfirmModal>
+ ) : null}
+ {authAvailableSet.size === 0 && (
+ <ConfirmModal
+ active={!noProvidersAck}
+ onCancel={() => setNoProvidersAck(true)}
+ description="No providers founds"
+ label="Add a provider manually"
+ onConfirm={async () => {
+ setManageProvider("");
+ }}
+ >
+ <p>
+ We have found no Anastasis providers for your chosen country /
+ currency. You can add a providers manually. To add a provider
+ you must know the provider URL (e.g. https://provider.com)
+ </p>
+ <p>
+ <a>Learn more about Anastasis providers</a>
+ </p>
+ </ConfirmModal>
+ )}
</div>
- <div class="column is-half">
+ <div class="column">
<p class="block">
- When recovering your wallet, you will be asked to verify your identity via the methods you configure here.
- The list of authentication method is defined by the backup provider list.
+ When recovering your secret data, you will be asked to verify your
+ identity via the methods you configure here. The list of
+ authentication method is defined by the backup provider list.
</p>
<p class="block">
- <button class="button is-info">Manage the backup provider's list</button>
+ <button
+ class="button is-info"
+ onClick={() => setManageProvider("")}
+ >
+ Manage backup providers
+ </button>
</p>
- {authAvailableSet.size > 0 && <p class="block">
- We couldn't find provider for some of the authentication methods.
- </p>}
+ {authAvailableSet.size > 0 && (
+ <p class="block">
+ We couldn't find provider for some of the authentication methods.
+ </p>
+ )}
</div>
</div>
</AnastasisClientFrame>
);
}
-type AuthMethodWithRemove = AuthMethod & { remove: () => void }
-export interface AuthMethodSetupProps {
- method: string;
- addAuthMethod: (x: any) => void;
- configured: AuthMethodWithRemove[];
- cancel: () => void;
-}
-
function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
return (
<AnastasisClientFrame hideNav title={`Add ${props.method} authentication`}>
@@ -186,36 +244,3 @@ function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode {
);
}
-
-function ConfirmModal({ active, description, onCancel, onConfirm, children, danger, disabled, label = 'Confirm' }: Props): VNode {
- return <div class={active ? "modal is-active" : "modal"}>
- <div class="modal-background " onClick={onCancel} />
- <div class="modal-card" style={{ maxWidth: 700 }}>
- <header class="modal-card-head">
- {!description ? null : <p class="modal-card-title"><b>{description}</b></p>}
- <button class="delete " aria-label="close" onClick={onCancel} />
- </header>
- <section class="modal-card-body">
- {children}
- </section>
- <footer class="modal-card-foot">
- <button class="button" onClick={onCancel} >Dismiss</button>
- <div class="buttons is-right" style={{ width: '100%' }}>
- <button class={danger ? "button is-danger " : "button is-info "} disabled={disabled} onClick={onConfirm} >{label}</button>
- </div>
- </footer>
- </div>
- <button class="modal-close is-large " aria-label="close" onClick={onCancel} />
- </div>
-}
-
-interface Props {
- active?: boolean;
- description?: string;
- onCancel?: () => void;
- onConfirm?: () => void;
- label?: string;
- children?: ComponentChildren;
- danger?: boolean;
- disabled?: boolean;
-}
diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
index b71a79727..c3ff7e746 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,48 +15,51 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { BackupFinishedScreen as TestedComponent } from './BackupFinishedScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { BackupFinishedScreen as TestedComponent } from "./BackupFinishedScreen";
export default {
- title: 'Pages/backup/FinishedScreen',
+ title: "Pages/backup/Finished",
component: TestedComponent,
args: {
- order: 9,
+ order: 8,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const WithoutName = createExample(TestedComponent, reducerStatesExample.backupFinished);
+export const WithoutName = createExample(
+ TestedComponent,
+ reducerStatesExample.backupFinished,
+);
-export const WithName = createExample(TestedComponent, {...reducerStatesExample.backupFinished,
- secret_name: 'super_secret',
+export const WithName = createExample(TestedComponent, {
+ ...reducerStatesExample.backupFinished,
+ secret_name: "super_secret",
} as ReducerState);
export const WithDetails = createExample(TestedComponent, {
...reducerStatesExample.backupFinished,
- secret_name: 'super_secret',
+ secret_name: "super_secret",
success_details: {
- 'http://anastasis.net': {
+ "https://anastasis.demo.taler.net/": {
policy_expiration: {
- t_ms: 'never'
+ t_ms: "never",
},
- policy_version: 0
+ policy_version: 0,
},
- 'http://taler.net': {
+ "https://kudos.demo.anastasis.lu/": {
policy_expiration: {
- t_ms: new Date().getTime() + 60*60*24*1000
+ t_ms: new Date().getTime() + 60 * 60 * 24 * 1000,
},
- policy_version: 1
+ policy_version: 1,
},
- }
+ },
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
index 7938baca4..129f1e9e4 100644
--- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx
@@ -1,44 +1,50 @@
+import { AuthenticationProviderStatusOk } from "anastasis-core";
import { format } from "date-fns";
import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function BackupFinishedScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const details = reducer.currentReducerState.success_details
+ const details = reducer.currentReducerState.success_details;
+ const providers = reducer.currentReducerState.authentication_providers ?? {}
- return (<AnastasisClientFrame hideNav title="Backup finished">
- {reducer.currentReducerState.secret_name ? <p>
- Your backup of secret <b>"{reducer.currentReducerState.secret_name}"</b> was
- successful.
- </p> :
- <p>
- Your secret was successfully backed up.
- </p>}
+ return (
+ <AnastasisClientFrame hideNav title="Backup success!">
+ <p>Your backup is complete.</p>
- {details && <div class="block">
- <p>The backup is stored by the following providers:</p>
- {Object.keys(details).map((x, i) => {
- const sd = details[x];
- return (
- <div key={i} class="box">
- {x}
- <p>
- version {sd.policy_version}
- {sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd-MM-yyyy')}` : ' without expiration date'}
- </p>
- </div>
- );
- })}
- </div>}
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={() => reducer.back()}>Back</button>
- </div>
- </AnastasisClientFrame>);
+ {details && (
+ <div class="block">
+ <p>The backup is stored by the following providers:</p>
+ {Object.keys(details).map((url, i) => {
+ const sd = details[url];
+ const p = providers[url] as AuthenticationProviderStatusOk
+ return (
+ <div key={i} class="box">
+ <a href={url} target="_blank" rel="noreferrer">{p.business_name}</a>
+ <p>
+ version {sd.policy_version}
+ {sd.policy_expiration.t_ms !== "never"
+ ? ` expires at: ${format(
+ new Date(sd.policy_expiration.t_ms),
+ "dd-MM-yyyy",
+ )}`
+ : " without expiration date"}
+ </p>
+ </div>
+ );
+ })}
+ </div>
+ )}
+ </AnastasisClientFrame>
+ );
}
diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
index 48115c798..56aee8763 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -20,12 +19,16 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { RecoveryStates, ReducerState } from "anastasis-core";
+import {
+ ChallengeFeedbackStatus,
+ RecoveryStates,
+ ReducerState,
+} from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen";
export default {
- title: "Pages/recovery/ChallengeOverviewScreen",
+ title: "Pages/recovery/SolveChallenge/Overview",
component: TestedComponent,
args: {
order: 5,
@@ -176,16 +179,15 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
recovery_information: {
policies: [
[
- { uuid: "1" },
- { uuid: "2" },
- { uuid: "3" },
- { uuid: "4" },
- { uuid: "5" },
- { uuid: "6" },
- { uuid: "7" },
- { uuid: "8" },
- { uuid: "9" },
- { uuid: "10" },
+ { uuid: "uuid-1" },
+ { uuid: "uuid-2" },
+ { uuid: "uuid-3" },
+ { uuid: "uuid-4" },
+ { uuid: "uuid-5" },
+ { uuid: "uuid-6" },
+ { uuid: "uuid-7" },
+ { uuid: "uuid-8" },
+ { uuid: "uuid-9" },
],
],
challenges: [
@@ -193,20 +195,96 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
cost: "USD:1",
instructions: 'in state "solved"',
type: "question",
- uuid: "1",
+ uuid: "uuid-1",
},
{
cost: "USD:1",
instructions: 'in state "message"',
type: "question",
- uuid: "2",
+ uuid: "uuid-2",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "auth iban"',
+ type: "question",
+ uuid: "uuid-3",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "payment "',
+ type: "question",
+ uuid: "uuid-4",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "rate limit"',
+ type: "question",
+ uuid: "uuid-5",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "redirect"',
+ type: "question",
+ uuid: "uuid-6",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "server failure"',
+ type: "question",
+ uuid: "uuid-7",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "truth unknown"',
+ type: "question",
+ uuid: "uuid-8",
+ },
+ {
+ cost: "USD:1",
+ instructions: 'in state "unsupported"',
+ type: "question",
+ uuid: "uuid-9",
},
],
},
challenge_feedback: {
- 1: { state: "solved" },
- 2: { state: "message", message: "Security question was not solved correctly" },
- // FIXME: add missing feedback states here!
+ "uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() },
+ "uuid-2": {
+ state: ChallengeFeedbackStatus.Message.toString(),
+ message: "Challenge should be solved",
+ },
+ "uuid-3": {
+ state: ChallengeFeedbackStatus.AuthIban.toString(),
+ challenge_amount: "EUR:1",
+ credit_iban: "DE12345789000",
+ business_name: "Data Loss Incorporated",
+ wire_transfer_subject: "Anastasis 987654321",
+ },
+ "uuid-4": {
+ state: ChallengeFeedbackStatus.Payment.toString(),
+ taler_pay_uri: "taler://pay/...",
+ provider: "https://localhost:8080/",
+ payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
+ },
+ "uuid-5": {
+ state: ChallengeFeedbackStatus.RateLimitExceeded.toString(),
+ // "error_code": 8121
+ },
+ "uuid-6": {
+ state: ChallengeFeedbackStatus.Redirect.toString(),
+ redirect_url: "https://videoconf.example.com/",
+ http_status: 303,
+ },
+ "uuid-7": {
+ state: ChallengeFeedbackStatus.ServerFailure.toString(),
+ http_status: 500,
+ error_response: "some error message or error object",
+ },
+ "uuid-8": {
+ state: ChallengeFeedbackStatus.TruthUnknown.toString(),
+ // "error_code": 8108
+ },
+ "uuid-9": { state: ChallengeFeedbackStatus.Unsupported.toString() },
},
} as ReducerState,
);
diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
index ed34bbde2..d0c9b2f5d 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx
@@ -3,6 +3,7 @@ import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
import { authMethods, KnownAuthMethods } from "./authMethod";
+import { AsyncButton } from "../../components/AsyncButton";
function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
const { feedback } = props;
@@ -11,28 +12,37 @@ function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
}
switch (feedback.state) {
case ChallengeFeedbackStatus.Message:
- return (
- <div>
- <p>{feedback.message}</p>
- </div>
- );
+ return <div class="block has-text-danger">{feedback.message}</div>;
+ case ChallengeFeedbackStatus.Solved:
+ return <div />;
case ChallengeFeedbackStatus.Pending:
case ChallengeFeedbackStatus.AuthIban:
return null;
+ case ChallengeFeedbackStatus.ServerFailure:
+ return <div class="block has-text-danger">Server error.</div>;
case ChallengeFeedbackStatus.RateLimitExceeded:
- return <div>Rate limit exceeded.</div>;
- case ChallengeFeedbackStatus.Redirect:
- return <div>Redirect (FIXME: not supported)</div>;
+ return (
+ <div class="block has-text-danger">
+ There were to many failed attempts.
+ </div>
+ );
case ChallengeFeedbackStatus.Unsupported:
- return <div>Challenge not supported by client.</div>;
+ return (
+ <div class="block has-text-danger">
+ This client doesn't support solving this type of challenge. Use
+ another version or contact the provider.
+ </div>
+ );
case ChallengeFeedbackStatus.TruthUnknown:
- return <div>Truth unknown</div>;
- default:
return (
- <div>
- <pre>{JSON.stringify(feedback)}</pre>
+ <div class="block has-text-danger">
+ Provider doesn't recognize the challenge of the policy. Contact the
+ provider for further information.
</div>
);
+ case ChallengeFeedbackStatus.Redirect:
+ default:
+ return <div />;
}
}
@@ -72,19 +82,25 @@ export function ChallengeOverviewScreen(): VNode {
feedback: challengeFeedback[ch.uuid],
};
}
- const policiesWithInfo = policies.map((row) => {
- let isPolicySolved = true;
- const challenges = row
- .map(({ uuid }) => {
- const info = knownChallengesMap[uuid];
- const isChallengeSolved = info?.feedback?.state === "solved";
- isPolicySolved = isPolicySolved && isChallengeSolved;
- return { info, uuid, isChallengeSolved };
- })
- .filter((ch) => ch.info !== undefined);
+ const policiesWithInfo = policies
+ .map((row) => {
+ let isPolicySolved = true;
+ const challenges = row
+ .map(({ uuid }) => {
+ const info = knownChallengesMap[uuid];
+ const isChallengeSolved = info?.feedback?.state === "solved";
+ isPolicySolved = isPolicySolved && isChallengeSolved;
+ return { info, uuid, isChallengeSolved };
+ })
+ .filter((ch) => ch.info !== undefined);
- return { isPolicySolved, challenges };
- });
+ return {
+ isPolicySolved,
+ challenges,
+ corrupted: row.length > challenges.length,
+ };
+ })
+ .filter((p) => !p.corrupted);
const atLeastThereIsOnePolicySolved =
policiesWithInfo.find((p) => p.isPolicySolved) !== undefined;
@@ -94,25 +110,124 @@ export function ChallengeOverviewScreen(): VNode {
: undefined;
return (
<AnastasisClientFrame hideNext={errors} title="Recovery: Solve challenges">
- {!policies.length ? (
+ {!policiesWithInfo.length ? (
<p class="block">
No policies found, try with another version of the secret
</p>
- ) : policies.length === 1 ? (
+ ) : policiesWithInfo.length === 1 ? (
<p class="block">
One policy found for this secret. You need to solve all the challenges
in order to recover your secret.
</p>
) : (
<p class="block">
- We have found {policies.length} polices. You need to solve all the
- challenges from one policy in order to recover your secret.
+ We have found {policiesWithInfo.length} polices. You need to solve all
+ the challenges from one policy in order to recover your secret.
</p>
)}
{policiesWithInfo.map((policy, policy_index) => {
const tableBody = policy.challenges.map(({ info, uuid }) => {
const isFree = !info.cost || info.cost.endsWith(":0");
const method = authMethods[info.type as KnownAuthMethods];
+
+ if (!method) {
+ return (
+ <div
+ key={uuid}
+ class="block"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <div style={{ display: "flex", alignItems: "center" }}>
+ <span>unknown challenge</span>
+ </div>
+ </div>
+ );
+ }
+
+ function ChallengeButton({
+ id,
+ feedback,
+ }: {
+ id: string;
+ feedback?: ChallengeFeedback;
+ }): VNode {
+ async function selectChallenge(): Promise<void> {
+ if (reducer) {
+ return reducer.transition("select_challenge", { uuid: id });
+ }
+ }
+ if (!feedback) {
+ return (
+ <div>
+ <AsyncButton
+ class="button"
+ disabled={
+ atLeastThereIsOnePolicySolved && !policy.isPolicySolved
+ }
+ onClick={selectChallenge}
+ >
+ Solve
+ </AsyncButton>
+ </div>
+ );
+ }
+ switch (feedback.state) {
+ case ChallengeFeedbackStatus.ServerFailure:
+ case ChallengeFeedbackStatus.Unsupported:
+ case ChallengeFeedbackStatus.TruthUnknown:
+ case ChallengeFeedbackStatus.RateLimitExceeded:
+ return <div />;
+ case ChallengeFeedbackStatus.AuthIban:
+ case ChallengeFeedbackStatus.Payment:
+ return (
+ <div>
+ <AsyncButton
+ class="button"
+ disabled={
+ atLeastThereIsOnePolicySolved && !policy.isPolicySolved
+ }
+ onClick={selectChallenge}
+ >
+ Pay
+ </AsyncButton>
+ </div>
+ );
+ case ChallengeFeedbackStatus.Redirect:
+ return (
+ <div>
+ <AsyncButton
+ class="button"
+ disabled={
+ atLeastThereIsOnePolicySolved && !policy.isPolicySolved
+ }
+ onClick={selectChallenge}
+ >
+ Go to {feedback.redirect_url}
+ </AsyncButton>
+ </div>
+ );
+ case ChallengeFeedbackStatus.Solved:
+ return (
+ <div>
+ <div class="tag is-success is-large">Solved</div>
+ </div>
+ );
+ default:
+ return (
+ <div>
+ <AsyncButton
+ class="button"
+ disabled={
+ atLeastThereIsOnePolicySolved && !policy.isPolicySolved
+ }
+ onClick={selectChallenge}
+ >
+ Solve
+ </AsyncButton>
+ </div>
+ );
+ }
+ }
return (
<div
key={uuid}
@@ -131,21 +246,8 @@ export function ChallengeOverviewScreen(): VNode {
</div>
<OverviewFeedbackDisplay feedback={info.feedback} />
</div>
- <div>
- {method && info.feedback?.state !== "solved" ? (
- <a
- class="button"
- onClick={() =>
- reducer.transition("select_challenge", { uuid })
- }
- >
- {isFree ? "Solve" : `Pay and Solve`}
- </a>
- ) : null}
- {info.feedback?.state === "solved" ? (
- <a class="button is-success"> Solved </a>
- ) : null}
- </div>
+
+ <ChallengeButton id={uuid} feedback={info.feedback} />
</div>
);
});
@@ -153,11 +255,13 @@ export function ChallengeOverviewScreen(): VNode {
const policyName = policy.challenges
.map((x) => x.info.type)
.join(" + ");
+
const opa = !atLeastThereIsOnePolicySolved
? undefined
: policy.isPolicySolved
? undefined
: "0.6";
+
return (
<div
key={policy_index}
diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
index e5fe09e99..8c788e556 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.stories.tsx
@@ -15,24 +15,26 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../utils';
-import { ChallengePayingScreen as TestedComponent } from './ChallengePayingScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../utils";
+import { ChallengePayingScreen as TestedComponent } from "./ChallengePayingScreen";
export default {
- title: 'Pages/recovery/__ChallengePayingScreen',
+ title: "Pages/recovery/__ChallengePaying",
component: TestedComponent,
args: {
order: 10,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Example = createExample(TestedComponent, reducerStatesExample.challengePaying);
+export const Example = createExample(
+ TestedComponent,
+ reducerStatesExample.challengePaying,
+);
diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx
index 84896a2ec..ffcc8fafc 100644
--- a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx
@@ -3,19 +3,19 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function ChallengePayingScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const payments = ['']; //reducer.currentReducerState.payments ??
+ const payments = [""]; //reducer.currentReducerState.payments ??
return (
- <AnastasisClientFrame
- hideNav
- title="Recovery: Challenge Paying"
- >
+ <AnastasisClientFrame hideNav title="Recovery: Challenge Paying">
<p>
Some of the providers require a payment to store the encrypted
authentication information.
diff --git a/packages/anastasis-webui/src/pages/home/ConfirmModal.tsx b/packages/anastasis-webui/src/pages/home/ConfirmModal.tsx
new file mode 100644
index 000000000..c9c59c1b4
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/ConfirmModal.tsx
@@ -0,0 +1,58 @@
+import { differenceInBusinessDays } from "date-fns";
+import { ComponentChildren, h, VNode } from "preact";
+import { useLayoutEffect, useRef } from "preact/hooks";
+import { AsyncButton } from "../../components/AsyncButton";
+
+export interface ConfirmModelProps {
+ active?: boolean;
+ description?: string;
+ onCancel?: () => void;
+ onConfirm?: () => Promise<void>;
+ label?: string;
+ cancelLabel?: string;
+ children?: ComponentChildren;
+ danger?: boolean;
+ disabled?: boolean;
+}
+
+export function ConfirmModal({
+ active, description, onCancel, onConfirm, children, danger, disabled, label = "Confirm", cancelLabel = "Dismiss"
+}: ConfirmModelProps): VNode {
+ return (
+ <div class={active ? "modal is-active" : "modal"} >
+ <div class="modal-background " onClick={onCancel} />
+ <div class="modal-card" style={{ maxWidth: 700 }}>
+ <header class="modal-card-head">
+ {!description ? null : (
+ <p class="modal-card-title">
+ <b>{description}</b>
+ </p>
+ )}
+ <button class="delete " aria-label="close" onClick={onCancel} />
+ </header>
+ <section class="modal-card-body">{children}</section>
+ <footer class="modal-card-foot">
+ <button class="button" onClick={onCancel}>
+ {cancelLabel}
+ </button>
+ <div class="buttons is-right" style={{ width: "100%" }} onKeyDown={(e) => {
+ if (e.key === 'Escape' && onCancel) onCancel()
+ }}>
+ <AsyncButton
+ grabFocus
+ class={danger ? "button is-danger " : "button is-info "}
+ disabled={disabled}
+ onClick={onConfirm}
+ >
+ {label}
+ </AsyncButton>
+ </div>
+ </footer>
+ </div>
+ <button
+ class="modal-close is-large "
+ aria-label="close"
+ onClick={onCancel} />
+ </div>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
index 6bdb3515d..0948d603e 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.stories.tsx
@@ -16,37 +16,42 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { ContinentSelectionScreen as TestedComponent } from "./ContinentSelectionScreen";
export default {
- title: 'Pages/Location',
+ title: "Pages/Location",
component: TestedComponent,
args: {
order: 2,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const BackupSelectContinent = createExample(TestedComponent, reducerStatesExample.backupSelectContinent);
+export const BackupSelectContinent = createExample(
+ TestedComponent,
+ reducerStatesExample.backupSelectContinent,
+);
export const BackupSelectCountry = createExample(TestedComponent, {
...reducerStatesExample.backupSelectContinent,
- selected_continent: 'Testcontinent',
+ selected_continent: "Testcontinent",
} as ReducerState);
-export const RecoverySelectContinent = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent);
+export const RecoverySelectContinent = createExample(
+ TestedComponent,
+ reducerStatesExample.recoverySelectContinent,
+);
export const RecoverySelectCountry = createExample(TestedComponent, {
...reducerStatesExample.recoverySelectContinent,
- selected_continent: 'Testcontinent',
+ selected_continent: "Testcontinent",
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
index 0e43f982d..aafde6e8c 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
@@ -1,58 +1,81 @@
/* eslint-disable @typescript-eslint/camelcase */
+import { BackupStates, RecoveryStates } from "anastasis-core";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame, withProcessLabel } from "./index";
export function ContinentSelectionScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
- //FIXME: remove this when #7056 is fixed
- const countryFromReducer = (reducer?.currentReducerState as any).selected_country || ""
- const [countryCode, setCountryCode] = useState( countryFromReducer )
+ // FIXME: remove this when #7056 is fixed
+ const countryFromReducer =
+ (reducer?.currentReducerState as any).selected_country || "";
+ const [countryCode, setCountryCode] = useState(countryFromReducer);
- if (!reducer || !reducer.currentReducerState || !("continents" in reducer.currentReducerState)) {
- return <div />
+ if (
+ !reducer ||
+ !reducer.currentReducerState ||
+ !("continents" in reducer.currentReducerState)
+ ) {
+ return <div />;
}
const selectContinent = (continent: string): void => {
- reducer.transition("select_continent", { continent })
+ reducer.transition("select_continent", { continent });
};
const selectCountry = (country: string): void => {
- setCountryCode(country)
+ setCountryCode(country);
};
-
-
+
const continentList = reducer.currentReducerState.continents || [];
const countryList = reducer.currentReducerState.countries || [];
- const theContinent = reducer.currentReducerState.selected_continent || ""
+ const theContinent = reducer.currentReducerState.selected_continent || "";
// const cc = reducer.currentReducerState.selected_country || "";
- const theCountry = countryList.find(c => c.code === countryCode)
- const selectCountryAction = () => {
+ const theCountry = countryList.find((c) => c.code === countryCode);
+ const selectCountryAction = async () => {
//selection should be when the select box changes it value
if (!theCountry) return;
reducer.transition("select_country", {
country_code: countryCode,
currencies: [theCountry.currency],
- })
- }
+ });
+ };
// const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
// reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting;
- const errors = !theCountry ? "Select a country" : undefined
+ const errors = !theCountry ? "Select a country" : undefined;
- return (
- <AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Where do you live?")} onNext={selectCountryAction}>
+ const handleBack = async () => {
+ // We want to go to the start, even if we already selected
+ // a country.
+ // FIXME: What if we don't want to lose all information here?
+ // Can we do some kind of soft reset?
+ reducer.reset();
+ };
- <div class="columns" >
+ return (
+ <AnastasisClientFrame
+ hideNext={errors}
+ title={withProcessLabel(reducer, "Where do you live?")}
+ onNext={selectCountryAction}
+ onBack={handleBack}
+ >
+ <div class="columns">
<div class="column is-one-third">
<div class="field">
<label class="label">Continent</label>
<div class="control is-expanded has-icons-left">
- <div class="select is-fullwidth" >
- <select onChange={(e) => selectContinent(e.currentTarget.value)} value={theContinent} >
- <option key="none" disabled selected value=""> Choose a continent </option>
- {continentList.map(prov => (
+ <div class="select is-fullwidth">
+ <select
+ onChange={(e) => selectContinent(e.currentTarget.value)}
+ value={theContinent}
+ >
+ <option key="none" disabled selected value="">
+ {" "}
+ Choose a continent{" "}
+ </option>
+ {continentList.map((prov) => (
<option key={prov.name} value={prov.name}>
{prov.name}
</option>
@@ -68,10 +91,17 @@ export function ContinentSelectionScreen(): VNode {
<div class="field">
<label class="label">Country</label>
<div class="control is-expanded has-icons-left">
- <div class="select is-fullwidth" >
- <select onChange={(e) => selectCountry((e.target as any).value)} disabled={!theContinent} value={theCountry?.code || ""}>
- <option key="none" disabled selected value=""> Choose a country </option>
- {countryList.map(prov => (
+ <div class="select is-fullwidth">
+ <select
+ onChange={(e) => selectCountry((e.target as any).value)}
+ disabled={!theContinent}
+ value={theCountry?.code || ""}
+ >
+ <option key="none" disabled selected value="">
+ {" "}
+ Choose a country{" "}
+ </option>
+ {countryList.map((prov) => (
<option key={prov.name} value={prov.code}>
{prov.name}
</option>
@@ -93,12 +123,37 @@ export function ContinentSelectionScreen(): VNode {
</div>
<div class="column is-two-third">
<p>
- Your location will help us to determine which personal information
- ask you for the next step.
+ Your selection will help us ask right information to uniquely
+ identify you when you want to recover your secret again.
+ </p>
+ <p>
+ Choose the country that issued most of your long-term legal
+ documents or personal identifiers.
</p>
+ <div
+ style={{
+ border: "1px solid gray",
+ borderRadius: "0.5em",
+ backgroundColor: "#fbfcbd",
+ padding: "0.5em",
+ }}
+ >
+ <p>
+ If you just want to try out Anastasis, we recomment that you
+ choose <b>Testcontinent</b> with <b>Demoland</b>. For this special
+ country, you will be asked for a simple number and not real,
+ personal identifiable information.
+ </p>
+ {/*
+ <p>
+ Because of the diversity of personally identifying information in
+ different countries and cultures, we do not support all countries
+ yet. If you want to improve the supported countries,{" "}
+ <a href="mailto:contact@anastasis.lu">contact us</a>.
+ </p> */}
+ </div>
</div>
</div>
-
</AnastasisClientFrame>
);
}
diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
index fc339e48e..4cbeb8308 100644
--- a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.stories.tsx
@@ -16,94 +16,126 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { EditPoliciesScreen as TestedComponent } from './EditPoliciesScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { EditPoliciesScreen as TestedComponent } from "./EditPoliciesScreen";
export default {
- title: 'Pages/backup/ReviewPoliciesScreen/EditPoliciesScreen',
+ title: "Pages/backup/ReviewPolicies/EditPolicies",
args: {
order: 6,
},
component: TestedComponent,
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const EditingAPolicy = createExample(TestedComponent, {
- ...reducerStatesExample.policyReview,
- policies: [{
- methods: [{
- authentication_method: 1,
- provider: 'https://anastasis.demo.taler.net/'
- }, {
- authentication_method: 2,
- provider: 'http://localhost:8086/'
- }]
- }, {
- methods: [{
- authentication_method: 1,
- provider: 'http://localhost:8086/'
- }]
- }],
- authentication_methods: [{
- type: "email",
- instructions: "Email to qwe@asd.com",
- challenge: "E5VPA"
- }, {
- type: "totp",
- instructions: "Response code for 'Anastasis'",
- challenge: "E5VPA"
- }, {
- type: "sms",
- instructions: "SMS to 6666-6666",
- challenge: ""
- }, {
- type: "question",
- instructions: "How did the chicken cross the road?",
- challenge: "C5SP8"
- }]
-} as ReducerState, { index : 0});
-
-export const CreatingAPolicy = createExample(TestedComponent, {
- ...reducerStatesExample.policyReview,
- policies: [{
- methods: [{
- authentication_method: 1,
- provider: 'https://anastasis.demo.taler.net/'
- }, {
- authentication_method: 2,
- provider: 'http://localhost:8086/'
- }]
- }, {
- methods: [{
- authentication_method: 1,
- provider: 'http://localhost:8086/'
- }]
- }],
- authentication_methods: [{
- type: "email",
- instructions: "Email to qwe@asd.com",
- challenge: "E5VPA"
- }, {
- type: "totp",
- instructions: "Response code for 'Anastasis'",
- challenge: "E5VPA"
- }, {
- type: "sms",
- instructions: "SMS to 6666-6666",
- challenge: ""
- }, {
- type: "question",
- instructions: "How did the chicken cross the road?",
- challenge: "C5SP8"
- }]
-} as ReducerState, { index : 3});
+export const EditingAPolicy = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.policyReview,
+ policies: [
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ {
+ authentication_method: 2,
+ provider: "http://localhost:8086/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "http://localhost:8086/",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [
+ {
+ type: "email",
+ instructions: "Email to qwe@asd.com",
+ challenge: "E5VPA",
+ },
+ {
+ type: "totp",
+ instructions: "Response code for 'Anastasis'",
+ challenge: "E5VPA",
+ },
+ {
+ type: "sms",
+ instructions: "SMS to 6666-6666",
+ challenge: "",
+ },
+ {
+ type: "question",
+ instructions: "How did the chicken cross the road?",
+ challenge: "C5SP8",
+ },
+ ],
+ } as ReducerState,
+ { index: 0 },
+);
+export const CreatingAPolicy = createExample(
+ TestedComponent,
+ {
+ ...reducerStatesExample.policyReview,
+ policies: [
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ {
+ authentication_method: 2,
+ provider: "http://localhost:8086/",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "http://localhost:8086/",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [
+ {
+ type: "email",
+ instructions: "Email to qwe@asd.com",
+ challenge: "E5VPA",
+ },
+ {
+ type: "totp",
+ instructions: "Response code for 'Anastasis'",
+ challenge: "E5VPA",
+ },
+ {
+ type: "sms",
+ instructions: "SMS to 6666-6666",
+ challenge: "",
+ },
+ {
+ type: "question",
+ instructions: "How did the chicken cross the road?",
+ challenge: "C5SP8",
+ },
+ ],
+ } as ReducerState,
+ { index: 3 },
+);
diff --git a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx
index 85cc96c46..198209399 100644
--- a/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/EditPoliciesScreen.tsx
@@ -20,7 +20,6 @@ interface Props {
index: number;
cancel: () => void;
confirm: (changes: MethodProvider[]) => void;
-
}
export interface MethodProvider {
@@ -28,106 +27,151 @@ export interface MethodProvider {
provider: string;
}
-export function EditPoliciesScreen({ index: policy_index, cancel, confirm }: Props): VNode {
- const [changedProvider, setChangedProvider] = useState<Array<string>>([])
+export function EditPoliciesScreen({
+ index: policy_index,
+ cancel,
+ confirm,
+}: Props): VNode {
+ const [changedProvider, setChangedProvider] = useState<Array<string>>([]);
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const selectableProviders: ProviderInfoByType = {}
- const allProviders = Object.entries(reducer.currentReducerState.authentication_providers || {})
+ const selectableProviders: ProviderInfoByType = {};
+ const allProviders = Object.entries(
+ reducer.currentReducerState.authentication_providers || {},
+ );
for (let index = 0; index < allProviders.length; index++) {
- const [url, status] = allProviders[index]
+ const [url, status] = allProviders[index];
if ("methods" in status) {
- status.methods.map(m => {
- const type: KnownAuthMethods = m.type as KnownAuthMethods
- const values = selectableProviders[type] || []
- const isFree = !m.usage_fee || m.usage_fee.endsWith(":0")
- values.push({ url, cost: m.usage_fee, isFree })
- selectableProviders[type] = values
- })
+ status.methods.map((m) => {
+ const type: KnownAuthMethods = m.type as KnownAuthMethods;
+ const values = selectableProviders[type] || [];
+ const isFree = !m.usage_fee || m.usage_fee.endsWith(":0");
+ values.push({ url, cost: m.usage_fee, isFree });
+ selectableProviders[type] = values;
+ });
}
}
- const allAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
+ const allAuthMethods =
+ reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? [];
- const policy = policies[policy_index]
-
- for(let method_index = 0; method_index < allAuthMethods.length; method_index++ ) {
- policy?.methods.find(m => m.authentication_method === method_index)?.provider
+ const policy = policies[policy_index];
+
+ for (
+ let method_index = 0;
+ method_index < allAuthMethods.length;
+ method_index++
+ ) {
+ policy?.methods.find((m) => m.authentication_method === method_index)
+ ?.provider;
}
function sendChanges(): void {
- const newMethods: MethodProvider[] = []
+ const newMethods: MethodProvider[] = [];
allAuthMethods.forEach((method, index) => {
- const oldValue = policy?.methods.find(m => m.authentication_method === index)
+ const oldValue = policy?.methods.find(
+ (m) => m.authentication_method === index,
+ );
if (changedProvider[index] === undefined && oldValue !== undefined) {
- newMethods.push(oldValue)
+ newMethods.push(oldValue);
}
- if (changedProvider[index] !== undefined && changedProvider[index] !== "") {
+ if (
+ changedProvider[index] !== undefined &&
+ changedProvider[index] !== ""
+ ) {
newMethods.push({
authentication_method: index,
- provider: changedProvider[index]
- })
+ provider: changedProvider[index],
+ });
}
- })
- confirm(newMethods)
+ });
+ confirm(newMethods);
}
- return <AnastasisClientFrame hideNav title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}>
- <section class="section">
- {!policy ? <p>
- Creating a new policy #{policy_index}
- </p> : <p>
- Editing policy #{policy_index}
- </p>}
- {allAuthMethods.map((method, index) => {
- //take the url from the updated change or from the policy
- const providerURL = changedProvider[index] === undefined ?
- policy?.methods.find(m => m.authentication_method === index)?.provider :
- changedProvider[index];
+ return (
+ <AnastasisClientFrame
+ hideNav
+ title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}
+ >
+ <section class="section">
+ {!policy ? (
+ <p>Creating a new policy #{policy_index}</p>
+ ) : (
+ <p>Editing policy #{policy_index}</p>
+ )}
+ {allAuthMethods.map((method, index) => {
+ //take the url from the updated change or from the policy
+ const providerURL =
+ changedProvider[index] === undefined
+ ? policy?.methods.find((m) => m.authentication_method === index)
+ ?.provider
+ : changedProvider[index];
- const type: KnownAuthMethods = method.type as KnownAuthMethods
- function changeProviderTo(url: string): void {
- const copy = [...changedProvider]
- copy[index] = url
- setChangedProvider(copy)
- }
- return (
- <div key={index} class="block" style={{ display: 'flex', alignItems: 'center' }}>
- <span class="icon">
- {authMethods[type]?.icon}
- </span>
- <span>
- {method.instructions}
- </span>
- <span>
- <span class="select " >
- <select onChange={(e) => changeProviderTo(e.currentTarget.value)} value={providerURL ?? ""}>
- <option key="none" value=""> &lt;&lt; off &gt;&gt; </option>
- {selectableProviders[type]?.map(prov => (
- <option key={prov.url} value={prov.url}>
- {prov.url}
+ const type: KnownAuthMethods = method.type as KnownAuthMethods;
+ function changeProviderTo(url: string): void {
+ const copy = [...changedProvider];
+ copy[index] = url;
+ setChangedProvider(copy);
+ }
+ return (
+ <div
+ key={index}
+ class="block"
+ style={{ display: "flex", alignItems: "center" }}
+ >
+ <span class="icon">{authMethods[type]?.icon}</span>
+ <span>{method.instructions}</span>
+ <span>
+ <span class="select ">
+ <select
+ onChange={(e) => changeProviderTo(e.currentTarget.value)}
+ value={providerURL ?? ""}
+ >
+ <option key="none" value="">
+ {" "}
+ &lt;&lt; off &gt;&gt;{" "}
</option>
- ))}
- </select>
+ {selectableProviders[type]?.map((prov) => (
+ <option key={prov.url} value={prov.url}>
+ {prov.url}
+ </option>
+ ))}
+ </select>
+ </span>
</span>
- </span>
- </div>
- );
- })}
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
- <span class="buttons">
- <button class="button" onClick={() => setChangedProvider([])}>Reset</button>
- <button class="button is-info" onClick={sendChanges}>Confirm</button>
- </span>
- </div>
- </section>
- </AnastasisClientFrame>
+ </div>
+ );
+ })}
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
+ <span class="buttons">
+ <button class="button" onClick={() => setChangedProvider([])}>
+ Reset
+ </button>
+ <button class="button is-info" onClick={sendChanges}>
+ Confirm
+ </button>
+ </span>
+ </div>
+ </section>
+ </AnastasisClientFrame>
+ );
}
diff --git a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
index e952ab28d..9bebcfbc9 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,35 +15,40 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { PoliciesPayingScreen as TestedComponent } from './PoliciesPayingScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { PoliciesPayingScreen as TestedComponent } from "./PoliciesPayingScreen";
export default {
- title: 'Pages/backup/PoliciesPayingScreen',
+ title: "Pages/backup/__PoliciesPaying",
component: TestedComponent,
args: {
- order: 8,
+ order: 9,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Example = createExample(TestedComponent, reducerStatesExample.policyPay);
+export const Example = createExample(
+ TestedComponent,
+ reducerStatesExample.policyPay,
+);
export const WithSomePaymentRequest = createExample(TestedComponent, {
...reducerStatesExample.policyPay,
- policy_payment_requests: [{
- payto: 'payto://x-taler-bank/bank.taler/account-a',
- provider: 'provider1'
- }, {
- payto: 'payto://x-taler-bank/bank.taler/account-b',
- provider: 'provider2'
- }]
+ policy_payment_requests: [
+ {
+ payto: "payto://x-taler-bank/bank.taler/account-a",
+ provider: "provider1",
+ },
+ {
+ payto: "payto://x-taler-bank/bank.taler/account-b",
+ provider: "provider2",
+ },
+ ],
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
index a470f5155..c3568b32d 100644
--- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx
@@ -3,20 +3,23 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function PoliciesPayingScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
const payments = reducer.currentReducerState.policy_payment_requests ?? [];
-
+
return (
<AnastasisClientFrame hideNav title="Backup: Recovery Document Payments">
<p>
- Some of the providers require a payment to store the encrypted
- recovery document.
+ Some of the providers require a payment to store the encrypted recovery
+ document.
</p>
<ul>
{payments.map((x, i) => {
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
index 0d2ebb778..1c05cd6e1 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,30 +15,41 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { RecoveryFinishedScreen as TestedComponent } from './RecoveryFinishedScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { RecoveryFinishedScreen as TestedComponent } from "./RecoveryFinishedScreen";
export default {
- title: 'Pages/recovery/FinishedScreen',
+ title: "Pages/recovery/Finished",
args: {
order: 7,
},
component: TestedComponent,
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
export const GoodEnding = createExample(TestedComponent, {
...reducerStatesExample.recoveryFinished,
- core_secret: { mime: 'text/plain', value: 'hello' }
+ recovery_document: {
+ secret_name: "the_name_of_the_secret",
+ },
+ core_secret: {
+ mime: "text/plain",
+ value: encodeCrock(
+ stringToBytes("hello this is my secret, don't tell anybody"),
+ ),
+ },
} as ReducerState);
-export const BadEnding = createExample(TestedComponent, reducerStatesExample.recoveryFinished);
+export const BadEnding = createExample(
+ TestedComponent,
+ reducerStatesExample.recoveryFinished,
+);
diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
index a61ef9efa..d83482559 100644
--- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx
@@ -1,39 +1,80 @@
-import {
- bytesToString,
- decodeCrock
-} from "@gnu-taler/taler-util";
+import { bytesToString, decodeCrock, encodeCrock } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { stringToBytes } from "qrcode-generator";
+import { QR } from "../../components/QR";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function RecoveryFinishedScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
+ const [copied, setCopied] = useState(false);
+ useEffect(() => {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }, [copied]);
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const encodedSecret = reducer.currentReducerState.core_secret
+ const secretName = reducer.currentReducerState.recovery_document?.secret_name;
+ const encodedSecret = reducer.currentReducerState.core_secret;
if (!encodedSecret) {
- return <AnastasisClientFrame title="Recovery Problem" hideNav>
- <p>
- Secret not found
- </p>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={() => reducer.back()}>Back</button>
- </div>
- </AnastasisClientFrame>
+ return (
+ <AnastasisClientFrame title="Recovery Problem" hideNav>
+ <p>Secret not found</p>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
}
- const secret = bytesToString(decodeCrock(encodedSecret.value))
+ const secret = bytesToString(decodeCrock(encodedSecret.value));
+ const contentURI = `data:${encodedSecret.mime},${secret}`;
+ // const fileName = encodedSecret['filename']
+ // data:plain/text;base64,asdasd
return (
- <AnastasisClientFrame title="Recovery Finished" hideNav>
- <p>
- Secret: {secret}
- </p>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={() => reducer.back()}>Back</button>
+ <AnastasisClientFrame title="Recovery Success" hideNav>
+ <h2 class="subtitle">Your secret was recovered</h2>
+ {secretName && (
+ <p class="block">
+ <b>Secret name:</b> {secretName}
+ </p>
+ )}
+ <div class="block buttons" disabled={copied}>
+ <button
+ class="button"
+ onClick={() => {
+ navigator.clipboard.writeText(secret);
+ setCopied(true);
+ }}
+ >
+ {!copied ? "Copy" : "Copied"}
+ </button>
+ <a class="button is-info" download="secret.txt" href={contentURI}>
+ <div class="icon is-small ">
+ <i class="mdi mdi-download" />
+ </div>
+ <span>Save as</span>
+ </a>
+ </div>
+ <div class="block">
+ <QR text={secret} />
</div>
</AnastasisClientFrame>
);
diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
index 9f7e26c16..4a1cba6a8 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,44 +15,51 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { ReviewPoliciesScreen as TestedComponent } from './ReviewPoliciesScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { ReviewPoliciesScreen as TestedComponent } from "./ReviewPoliciesScreen";
export default {
- title: 'Pages/backup/ReviewPoliciesScreen',
+ title: "Pages/backup/ReviewPolicies",
args: {
order: 6,
},
component: TestedComponent,
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, {
...reducerStatesExample.policyReview,
- policies: [{
- methods: [{
- authentication_method: 0,
- provider: 'asd'
- }, {
- authentication_method: 1,
- provider: 'asd'
- }]
- }, {
- methods: [{
- authentication_method: 1,
- provider: 'asd'
- }]
- }],
- authentication_methods: []
+ policies: [
+ {
+ methods: [
+ {
+ authentication_method: 0,
+ provider: "asd",
+ },
+ {
+ authentication_method: 1,
+ provider: "asd",
+ },
+ ],
+ },
+ {
+ methods: [
+ {
+ authentication_method: 1,
+ provider: "asd",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [],
} as ReducerState);
export const SomePoliciesWithMethods = createExample(TestedComponent, {
@@ -63,186 +69,193 @@ export const SomePoliciesWithMethods = createExample(TestedComponent, {
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
- }
- ]
+ provider: "https://kudos.demo.anastasis.lu/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 0,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
+ provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 1,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
+ provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
},
{
methods: [
{
authentication_method: 2,
- provider: "https://kudos.demo.anastasis.lu/"
+ provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
- provider: "https://anastasis.demo.taler.net/"
+ provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
- provider: "https://anastasis.demo.taler.net/"
- }
- ]
- }
+ provider: "https://anastasis.demo.taler.net/",
+ },
+ ],
+ },
+ ],
+ authentication_methods: [
+ {
+ type: "email",
+ instructions: "Email to qwe@asd.com",
+ challenge: "E5VPA",
+ },
+ {
+ type: "sms",
+ instructions: "SMS to 555-555",
+ challenge: "",
+ },
+ {
+ type: "question",
+ instructions: "Does P equal NP?",
+ challenge: "C5SP8",
+ },
+ {
+ type: "totp",
+ instructions: "Response code for 'Anastasis'",
+ challenge: "E5VPA",
+ },
+ {
+ type: "sms",
+ instructions: "SMS to 6666-6666",
+ challenge: "",
+ },
+ {
+ type: "question",
+ instructions: "How did the chicken cross the road?",
+ challenge: "C5SP8",
+ },
],
- authentication_methods: [{
- type: "email",
- instructions: "Email to qwe@asd.com",
- challenge: "E5VPA"
- }, {
- type: "sms",
- instructions: "SMS to 555-555",
- challenge: ""
- }, {
- type: "question",
- instructions: "Does P equal NP?",
- challenge: "C5SP8"
- },{
- type: "totp",
- instructions: "Response code for 'Anastasis'",
- challenge: "E5VPA"
- }, {
- type: "sms",
- instructions: "SMS to 6666-6666",
- challenge: ""
- }, {
- type: "question",
- instructions: "How did the chicken cross the road?",
- challenge: "C5SP8"
-}]
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
index f93963f67..0ed08e037 100644
--- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx
@@ -1,24 +1,33 @@
-/* eslint-disable @typescript-eslint/camelcase */
+import { AuthenticationProviderStatusOk } from "anastasis-core";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
+import { AsyncButton } from "../../components/AsyncButton";
import { useAnastasisContext } from "../../context/anastasis";
import { authMethods, KnownAuthMethods } from "./authMethod";
+import { ConfirmModal } from "./ConfirmModal";
import { EditPoliciesScreen } from "./EditPoliciesScreen";
import { AnastasisClientFrame } from "./index";
export function ReviewPoliciesScreen(): VNode {
- const [editingPolicy, setEditingPolicy] = useState<number | undefined>()
- const reducer = useAnastasisContext()
+ const [editingPolicy, setEditingPolicy] = useState<number | undefined>();
+ const [confirmReset, setConfirmReset] = useState(false);
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
- const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
+ const configuredAuthMethods =
+ reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? [];
+ const providers = reducer.currentReducerState.authentication_providers ?? {};
+
if (editingPolicy !== undefined) {
return (
<EditPoliciesScreen
@@ -29,62 +38,145 @@ export function ReviewPoliciesScreen(): VNode {
policy_index: editingPolicy,
policy: newMethods,
});
- setEditingPolicy(undefined)
+ setEditingPolicy(undefined);
}}
/>
- )
+ );
+ }
+ async function resetPolicies(): Promise<void> {
+ if (!reducer) return Promise.resolve();
+ return reducer.runTransaction(async (tx) => {
+ await tx.transition("back", {});
+ await tx.transition("next", {});
+ setConfirmReset(false);
+ });
}
- const errors = policies.length < 1 ? 'Need more policies' : undefined
+ const errors = policies.length < 1 ? "Need more policies" : undefined;
return (
- <AnastasisClientFrame hideNext={errors} title="Backup: Review Recovery Policies">
- {policies.length > 0 && <p class="block">
- Based on your configured authentication method you have created, some policies
- have been configured. In order to recover your secret you have to solve all the
- challenges of at least one policy.
- </p>}
- {policies.length < 1 && <p class="block">
- No policies had been created. Go back and add more authentication methods.
- </p>}
- <div class="block" style={{ justifyContent: 'flex-end' }} >
- <button class="button is-success" onClick={() => setEditingPolicy(policies.length + 1)}>Add new policy</button>
+ <AnastasisClientFrame
+ hideNext={errors}
+ title="Backup: Review Recovery Policies"
+ >
+ {policies.length > 0 && (
+ <p class="block">
+ Based on your configured authentication method you have created, some
+ policies have been configured. In order to recover your secret you
+ have to solve all the challenges of at least one policy.
+ </p>
+ )}
+ {policies.length < 1 && (
+ <p class="block">
+ No policies had been created. Go back and add more authentication
+ methods.
+ </p>
+ )}
+ <div class="block">
+ <AsyncButton class="button" onClick={async () => setConfirmReset(true)}>
+ Reset policies
+ </AsyncButton>
+ <button
+ class="button is-success"
+ style={{ marginLeft: 10 }}
+ onClick={() => setEditingPolicy(policies.length)}
+ >
+ Add new policy
+ </button>
</div>
{policies.map((p, policy_index) => {
const methods = p.methods
- .map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider }))
- .filter(x => !!x)
+ .map(
+ (x) =>
+ configuredAuthMethods[x.authentication_method] && {
+ ...configuredAuthMethods[x.authentication_method],
+ provider: x.provider,
+ },
+ )
+ .filter((x) => !!x);
+
+ const policyName = methods.map((x) => x.type).join(" + ");
- const policyName = methods.map(x => x.type).join(" + ");
+ if (p.methods.length > methods.length) {
+ //there is at least one authentication method that is corrupted
+ return null;
+ }
return (
- <div key={policy_index} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
+ <div
+ key={policy_index}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
<div>
<h3 class="subtitle">
Policy #{policy_index + 1}: {policyName}
</h3>
- {!methods.length && <p>
- No auth method found
- </p>}
+ {!methods.length && <p>No auth method found</p>}
{methods.map((m, i) => {
+ const p = providers[
+ m.provider
+ ] as AuthenticationProviderStatusOk;
return (
- <p key={i} class="block" style={{ display: 'flex', alignItems: 'center' }}>
+ <p
+ key={i}
+ class="block"
+ style={{ display: "flex", alignItems: "center" }}
+ >
<span class="icon">
{authMethods[m.type as KnownAuthMethods]?.icon}
</span>
<span>
- {m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a>
+ {m.instructions} recovery provided by{" "}
+ <a href={m.provider} target="_blank" rel="noreferrer">
+ {p.business_name}
+ </a>
</span>
</p>
);
})}
</div>
- <div style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}>
- <button class="button is-info block" onClick={() => setEditingPolicy(policy_index)}>Edit</button>
- <button class="button is-danger block" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button>
+ <div
+ style={{
+ marginTop: "auto",
+ marginBottom: "auto",
+ display: "flex",
+ justifyContent: "space-between",
+ flexDirection: "column",
+ }}
+ >
+ <button
+ class="button is-info block"
+ onClick={() => setEditingPolicy(policy_index)}
+ >
+ Edit
+ </button>
+ <button
+ class="button is-danger block"
+ onClick={() =>
+ reducer.transition("delete_policy", { policy_index })
+ }
+ >
+ Delete
+ </button>
</div>
</div>
);
})}
+ {confirmReset && (
+ <ConfirmModal
+ active
+ onCancel={() => setConfirmReset(false)}
+ description="Do you want to reset the policies to default state?"
+ label="Reset policies"
+ cancelLabel="Cancel"
+ onConfirm={resetPolicies}
+ >
+ <p>
+ All policies will be recalculated based on the authentication
+ providers configured and any change that you did will be lost
+ </p>
+ </ConfirmModal>
+ )}
</AnastasisClientFrame>
);
}
diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
index 49dd8fca8..3f2c6a245 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,30 +15,29 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { SecretEditorScreen as TestedComponent } from './SecretEditorScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { SecretEditorScreen as TestedComponent } from "./SecretEditorScreen";
export default {
- title: 'Pages/backup/SecretEditorScreen',
+ title: "Pages/backup/SecretInput",
component: TestedComponent,
args: {
order: 7,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
export const WithSecretNamePreselected = createExample(TestedComponent, {
...reducerStatesExample.secretEdition,
- secret_name: 'someSecretName',
+ secret_name: "someSecretName",
} as ReducerState);
export const WithoutName = createExample(TestedComponent, {
diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
index 1b36a1b21..6d4ffbf88 100644
--- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx
@@ -1,41 +1,56 @@
-/* eslint-disable @typescript-eslint/camelcase */
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis";
-import {
- AnastasisClientFrame
-} from "./index";
+import { AnastasisClientFrame } from "./index";
import { TextInput } from "../../components/fields/TextInput";
-import { FileInput } from "../../components/fields/FileInput";
+import { FileInput, FileTypeContent } from "../../components/fields/FileInput";
export function SecretEditorScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
const [secretValue, setSecretValue] = useState("");
+ const [secretFile, _setSecretFile] = useState<FileTypeContent | undefined>(
+ undefined,
+ );
+ function setSecretFile(v: FileTypeContent | undefined): void {
+ setSecretValue(""); // reset secret value when uploading a file
+ _setSecretFile(v);
+ }
- const currentSecretName = reducer?.currentReducerState
- && ("secret_name" in reducer.currentReducerState)
- && reducer.currentReducerState.secret_name;
+ const currentSecretName =
+ reducer?.currentReducerState &&
+ "secret_name" in reducer.currentReducerState &&
+ reducer.currentReducerState.secret_name;
const [secretName, setSecretName] = useState(currentSecretName || "");
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
const secretNext = async (): Promise<void> => {
+ const secret = secretFile
+ ? {
+ value: encodeCrock(stringToBytes(secretValue)),
+ filename: secretFile.name,
+ mime: secretFile.type,
+ }
+ : {
+ value: encodeCrock(stringToBytes(secretValue)),
+ mime: "text/plain",
+ };
return reducer.runTransaction(async (tx) => {
await tx.transition("enter_secret_name", {
name: secretName,
});
await tx.transition("enter_secret", {
- secret: {
- value: encodeCrock(stringToBytes(secretValue)),
- mime: "text/plain",
- },
+ secret,
expiration: {
t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
},
@@ -43,31 +58,46 @@ export function SecretEditorScreen(): VNode {
await tx.transition("next", {});
});
};
+ const errors = !secretName
+ ? "Add a secret name"
+ : !secretValue && !secretFile
+ ? "Add a secret value or a choose a file to upload"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) secretNext();
+ }
return (
<AnastasisClientFrame
+ hideNext={errors}
title="Backup: Provide secret to backup"
onNext={() => secretNext()}
>
- <div>
+ <div class="block">
<TextInput
- label="Secret's name:"
+ label="Secret name:"
+ tooltip="The secret name allows you to identify your secret when restoring it. It is a label that you can choose freely."
grabFocus
+ onConfirm={goNextIfNoErrors}
bind={[secretName, setSecretName]}
/>
</div>
- <div>
+ <div class="block">
<TextInput
+ disabled={!!secretFile}
+ onConfirm={goNextIfNoErrors}
label="Enter the secret as text:"
bind={[secretValue, setSecretValue]}
/>
- <div style={{display:'flex',}}>
- or&nbsp;
- <FileInput
- label="click here"
- bind={[secretValue, setSecretValue]}
- />
- &nbsp;to import a file
- </div>
+ </div>
+ <div class="block">
+ Or upload a secret file
+ <FileInput label="Choose file" onChange={setSecretFile} />
+ {secretFile && (
+ <div>
+ Uploading secret file <b>{secretFile.name}</b>{" "}
+ <a onClick={() => setSecretFile(undefined)}>cancel</a>
+ </div>
+ )}
</div>
</AnastasisClientFrame>
);
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
index 6919eebad..01ce3f0a7 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.stories.tsx
@@ -15,37 +15,35 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { SecretSelectionScreen as TestedComponent } from './SecretSelectionScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { SecretSelectionScreen as TestedComponent } from "./SecretSelectionScreen";
export default {
- title: 'Pages/recovery/SecretSelectionScreen',
+ title: "Pages/recovery/SecretSelection",
component: TestedComponent,
args: {
order: 4,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
export const Example = createExample(TestedComponent, {
...reducerStatesExample.secretSelection,
recovery_document: {
- provider_url: 'https://kudos.demo.anastasis.lu/',
- secret_name: 'secretName',
+ provider_url: "https://kudos.demo.anastasis.lu/",
+ secret_name: "secretName",
version: 1,
},
} as ReducerState);
-
export const NoRecoveryDocumentFound = createExample(TestedComponent, {
...reducerStatesExample.secretSelection,
recovery_document: undefined,
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
index 8aa5ed2f7..7e517abfe 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
@@ -1,23 +1,31 @@
+import { AuthenticationProviderStatus, AuthenticationProviderStatusOk } from "anastasis-core";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../components/AsyncButton";
-import { NumberInput } from "../../components/fields/NumberInput";
+import { PhoneNumberInput } from "../../components/fields/NumberInput";
import { useAnastasisContext } from "../../context/anastasis";
+import { AddingProviderScreen } from "./AddingProviderScreen";
import { AnastasisClientFrame } from "./index";
export function SecretSelectionScreen(): VNode {
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
- const currentVersion = (reducer?.currentReducerState
- && ("recovery_document" in reducer.currentReducerState)
- && reducer.currentReducerState.recovery_document?.version) || 0;
+ const [manageProvider, setManageProvider] = useState(false);
+ const currentVersion =
+ (reducer?.currentReducerState &&
+ "recovery_document" in reducer.currentReducerState &&
+ reducer.currentReducerState.recovery_document?.version) ||
+ 0;
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
async function doSelectVersion(p: string, n: number): Promise<void> {
@@ -31,66 +39,108 @@ export function SecretSelectionScreen(): VNode {
});
}
- const providerList = Object.keys(reducer.currentReducerState.authentication_providers ?? {})
- const recoveryDocument = reducer.currentReducerState.recovery_document
+ const provs = reducer.currentReducerState.authentication_providers ?? {};
+ const recoveryDocument = reducer.currentReducerState.recovery_document;
if (!recoveryDocument) {
- return <ChooseAnotherProviderScreen
- providers={providerList} selected=""
- onChange={(newProv) => doSelectVersion(newProv, 0)}
- />
+ return (
+ <ChooseAnotherProviderScreen
+ providers={provs}
+ selected=""
+ onChange={(newProv) => doSelectVersion(newProv, 0)}
+ />
+ );
}
if (selectingVersion) {
- return <SelectOtherVersionProviderScreen providers={providerList}
- provider={recoveryDocument.provider_url} version={recoveryDocument.version}
- onCancel={() => setSelectingVersion(false)}
- onConfirm={doSelectVersion}
- />
+ return (
+ <SelectOtherVersionProviderScreen
+ providers={provs}
+ provider={recoveryDocument.provider_url}
+ version={recoveryDocument.version}
+ onCancel={() => setSelectingVersion(false)}
+ onConfirm={doSelectVersion}
+ />
+ );
}
+ if (manageProvider) {
+ return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
+ }
+
+ const provierInfo = provs[recoveryDocument.provider_url] as AuthenticationProviderStatusOk
return (
<AnastasisClientFrame title="Recovery: Select secret">
<div class="columns">
<div class="column">
- <div class="box" style={{ border: '2px solid green' }}>
- <h1 class="subtitle">{recoveryDocument.provider_url}</h1>
+ <div class="box" style={{ border: "2px solid green" }}>
+ <h1 class="subtitle">
+ {provierInfo.business_name}
+ </h1>
<div class="block">
- {currentVersion === 0 ? <p>
- Set to recover the latest version
- </p> : <p>
- Set to recover the version number {currentVersion}
- </p>}
+ {currentVersion === 0 ? (
+ <p>Set to recover the latest version</p>
+ ) : (
+ <p>Set to recover the version number {currentVersion}</p>
+ )}
</div>
<div class="buttons is-right">
- <button class="button" onClick={(e) => setSelectingVersion(true)}>Change secret's version</button>
+ <button class="button" onClick={(e) => setSelectingVersion(true)}>
+ Change secret's version
+ </button>
</div>
</div>
</div>
<div class="column">
- <p>Secret found, you can select another version or continue to the challenges solving</p>
+ <p>
+ Secret found, you can select another version or continue to the
+ challenges solving
+ </p>
+ <p class="block">
+ <a onClick={() => setManageProvider(true)}>
+ Manage recovery providers
+ </a>
+ </p>
</div>
</div>
</AnastasisClientFrame>
);
}
-
-function ChooseAnotherProviderScreen({ providers, selected, onChange }: { selected: string; providers: string[]; onChange: (prov: string) => void }): VNode {
+function ChooseAnotherProviderScreen({
+ providers,
+ selected,
+ onChange,
+}: {
+ selected: string;
+ providers: { [url: string]: AuthenticationProviderStatus };
+ onChange: (prov: string) => void;
+}): VNode {
return (
- <AnastasisClientFrame hideNext="Recovery document not found" title="Recovery: Problem">
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery: Problem"
+ >
<p>No recovery document found, try with another provider</p>
<div class="field">
<label class="label">Provider</label>
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth">
- <select onChange={(e) => onChange(e.currentTarget.value)} value={selected}>
- <option key="none" disabled selected value=""> Choose a provider </option>
- {providers.map(prov => (
- <option key={prov} value={prov}>
- {prov}
+ <select
+ onChange={(e) => onChange(e.currentTarget.value)}
+ value={selected}
+ >
+ <option key="none" disabled selected value="">
+ {" "}
+ Choose a provider{" "}
+ </option>
+ {Object.keys(providers).map((url) => {
+ const p = providers[url]
+ if (!("methods" in p)) return null
+ return <option key={url} value={url}>
+ {p.business_name}
</option>
- ))}
+ })}
</select>
<div class="icon is-small is-left">
<i class="mdi mdi-earth" />
@@ -102,22 +152,37 @@ function ChooseAnotherProviderScreen({ providers, selected, onChange }: { select
);
}
-function SelectOtherVersionProviderScreen({ providers, provider, version, onConfirm, onCancel }: { onCancel: () => void; provider: string; version: number; providers: string[]; onConfirm: (prov: string, v: number) => Promise<void>; }): VNode {
+function SelectOtherVersionProviderScreen({
+ providers,
+ provider,
+ version,
+ onConfirm,
+ onCancel,
+}: {
+ onCancel: () => void;
+ provider: string;
+ version: number;
+ providers: { [url: string]: AuthenticationProviderStatus };
+ onConfirm: (prov: string, v: number) => Promise<void>;
+}): VNode {
const [otherProvider, setOtherProvider] = useState<string>(provider);
- const [otherVersion, setOtherVersion] = useState(`${version}`);
+ const [otherVersion, setOtherVersion] = useState(
+ version > 0 ? String(version) : "",
+ );
+ const otherProviderInfo = providers[otherProvider] as AuthenticationProviderStatusOk
return (
<AnastasisClientFrame hideNav title="Recovery: Select secret">
<div class="columns">
<div class="column">
<div class="box">
- <h1 class="subtitle">Provider {otherProvider}</h1>
+ <h1 class="subtitle">Provider {otherProviderInfo.business_name}</h1>
<div class="block">
- {version === 0 ? <p>
- Set to recover the latest version
- </p> : <p>
- Set to recover the version number {version}
- </p>}
+ {version === 0 ? (
+ <p>Set to recover the latest version</p>
+ ) : (
+ <p>Set to recover the version number {version}</p>
+ )}
<p>Specify other version below or use the latest</p>
</div>
@@ -125,13 +190,21 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
<label class="label">Provider</label>
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth">
- <select onChange={(e) => setOtherProvider(e.currentTarget.value)} value={otherProvider}>
- <option key="none" disabled selected value=""> Choose a provider </option>
- {providers.map(prov => (
- <option key={prov} value={prov}>
- {prov}
+ <select
+ onChange={(e) => setOtherProvider(e.currentTarget.value)}
+ value={otherProvider}
+ >
+ <option key="none" disabled selected value="">
+ {" "}
+ Choose a provider{" "}
+ </option>
+ {Object.keys(providers).map((url) => {
+ const p = providers[url]
+ if (!("methods" in p)) return null
+ return <option key={url} value={url}>
+ {p.business_name}
</option>
- ))}
+ })}
</select>
<div class="icon is-small is-left">
<i class="mdi mdi-earth" />
@@ -140,27 +213,43 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
</div>
</div>
<div class="container">
- <NumberInput
+ <PhoneNumberInput
label="Version"
placeholder="version number to recover"
grabFocus
- bind={[otherVersion, setOtherVersion]} />
+ bind={[otherVersion, setOtherVersion]}
+ />
</div>
</div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={onCancel}>Cancel</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
<div class="buttons">
- <AsyncButton class="button" onClick={() => onConfirm(otherProvider, 0)}>Use latest</AsyncButton>
- <AsyncButton class="button is-info" onClick={() => onConfirm(otherProvider, parseInt(otherVersion, 10))}>Confirm</AsyncButton>
+ <AsyncButton
+ class="button"
+ onClick={() => onConfirm(otherProvider, 0)}
+ >
+ Use latest
+ </AsyncButton>
+ <AsyncButton
+ class="button is-info"
+ onClick={() =>
+ onConfirm(otherProvider, parseInt(otherVersion, 10))
+ }
+ >
+ Confirm
+ </AsyncButton>
</div>
</div>
</div>
- <div class="column">
- .
- </div>
</div>
-
</AnastasisClientFrame>
);
-
}
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
index cb6561b3f..76d0700db 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,109 +15,63 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { SolveScreen as TestedComponent } from './SolveScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import {
+ ChallengeFeedbackStatus,
+ RecoveryStates,
+ ReducerState,
+} from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { SolveScreen as TestedComponent } from "./SolveScreen";
export default {
- title: 'Pages/recovery/SolveScreen',
+ title: "Pages/recovery/SolveChallenge/Solve",
component: TestedComponent,
args: {
order: 6,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const NoInformation = createExample(TestedComponent, reducerStatesExample.challengeSolving);
+export const NoInformation = createExample(
+ TestedComponent,
+ reducerStatesExample.challengeSolving,
+);
export const NotSupportedChallenge = createExample(TestedComponent, {
...reducerStatesExample.challengeSolving,
recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'does P equals NP?',
- type: 'chall-type',
- uuid: 'ASDASDSAD!1'
- }],
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "chall-type",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
policies: [],
},
- selected_challenge_uuid: 'ASDASDSAD!1'
+ selected_challenge_uuid: "ASDASDSAD!1",
} as ReducerState);
export const MismatchedChallengeId = createExample(TestedComponent, {
...reducerStatesExample.challengeSolving,
recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'does P equals NP?',
- type: 'chall-type',
- uuid: 'ASDASDSAD!1'
- }],
- policies: [],
- },
- selected_challenge_uuid: 'no-no-no'
-} as ReducerState);
-
-export const SmsChallenge = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSolving,
- recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'SMS to 555-5555',
- type: 'sms',
- uuid: 'ASDASDSAD!1'
- }],
- policies: [],
- },
- selected_challenge_uuid: 'ASDASDSAD!1'
-} as ReducerState);
-
-export const QuestionChallenge = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSolving,
- recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'does P equals NP?',
- type: 'question',
- uuid: 'ASDASDSAD!1'
- }],
- policies: [],
- },
- selected_challenge_uuid: 'ASDASDSAD!1'
-} as ReducerState);
-
-export const EmailChallenge = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSolving,
- recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'Email to sebasjm@some-domain.com',
- type: 'email',
- uuid: 'ASDASDSAD!1'
- }],
- policies: [],
- },
- selected_challenge_uuid: 'ASDASDSAD!1'
-} as ReducerState);
-
-export const PostChallenge = createExample(TestedComponent, {
- ...reducerStatesExample.challengeSolving,
- recovery_information: {
- challenges: [{
- cost: 'USD:1',
- instructions: 'Letter to address in postal code ABC123',
- type: 'post',
- uuid: 'ASDASDSAD!1'
- }],
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "chall-type",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
policies: [],
},
- selected_challenge_uuid: 'ASDASDSAD!1'
+ selected_challenge_uuid: "no-no-no",
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
index bc1a88db3..b87dad2ce 100644
--- a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx
@@ -1,50 +1,132 @@
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
+import { h, VNode } from "preact";
import { AnastasisClientFrame } from ".";
import {
ChallengeFeedback,
ChallengeFeedbackStatus,
- ChallengeInfo,
} from "../../../../anastasis-core/lib";
-import { AsyncButton } from "../../components/AsyncButton";
-import { TextInput } from "../../components/fields/TextInput";
+import { Notifications } from "../../components/Notifications";
import { useAnastasisContext } from "../../context/anastasis";
+import { authMethods, KnownAuthMethods } from "./authMethod";
-function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
+export function SolveOverviewFeedbackDisplay(props: {
+ feedback?: ChallengeFeedback;
+}): VNode {
const { feedback } = props;
if (!feedback) {
- return null;
+ return <div />;
}
switch (feedback.state) {
case ChallengeFeedbackStatus.Message:
return (
- <div>
- <p>{feedback.message}</p>
- </div>
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Message from provider`,
+ description: feedback.message,
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.Payment:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Message from provider`,
+ description: (
+ <span>
+ To pay you can <a href={feedback.taler_pay_uri}>click here</a>
+ </span>
+ ),
+ },
+ ]}
+ />
);
- case ChallengeFeedbackStatus.Pending:
case ChallengeFeedbackStatus.AuthIban:
- return null;
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Message from provider`,
+ description: `Need to send a wire transfer to "${feedback.business_name}"`,
+ },
+ ]}
+ />
+ );
+ case ChallengeFeedbackStatus.ServerFailure:
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Server error: Code ${feedback.http_status}`,
+ description: feedback.error_response,
+ },
+ ]}
+ />
+ );
case ChallengeFeedbackStatus.RateLimitExceeded:
- return <div>Rate limit exceeded.</div>;
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Message from provider`,
+ description: "There were to many failed attempts.",
+ },
+ ]}
+ />
+ );
case ChallengeFeedbackStatus.Redirect:
- return <div>Redirect (FIXME: not supported)</div>;
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "INFO",
+ message: `Message from provider`,
+ description: (
+ <span>
+ Please visit this link: <a>{feedback.redirect_url}</a>
+ </span>
+ ),
+ },
+ ]}
+ />
+ );
case ChallengeFeedbackStatus.Unsupported:
- return <div>Challenge not supported by client.</div>;
+ return (
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `This client doesn't support solving this type of challenge`,
+ description: `Use another version or contact the provider. Type of challenge "${feedback.unsupported_method}"`,
+ },
+ ]}
+ />
+ );
case ChallengeFeedbackStatus.TruthUnknown:
- return <div>Truth unknown</div>;
- default:
return (
- <div>
- <pre>{JSON.stringify(feedback)}</pre>
- </div>
+ <Notifications
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Provider doesn't recognize the type of challenge`,
+ description: "Contact the provider for further information",
+ },
+ ]}
+ />
);
+ default:
+ return <div />;
}
}
export function SolveScreen(): VNode {
const reducer = useAnastasisContext();
- const [answer, setAnswer] = useState("");
if (!reducer) {
return (
@@ -78,161 +160,54 @@ export function SolveScreen(): VNode {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={() => reducer.back()}>Back</button>
- </div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+ function SolveNotImplemented(): VNode {
+ return (
+ <AnastasisClientFrame hideNav title="Not implemented">
+ <p>
+ The challenge selected is not supported for this UI. Please update
+ this version or try using another policy.
+ </p>
+ {reducer && (
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ )}
</AnastasisClientFrame>
);
}
const chArr = reducer.currentReducerState.recovery_information.challenges;
- const challengeFeedback =
- reducer.currentReducerState.challenge_feedback ?? {};
const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
- const challenges: {
- [uuid: string]: ChallengeInfo;
- } = {};
- for (const ch of chArr) {
- challenges[ch.uuid] = ch;
- }
- const selectedChallenge = challenges[selectedUuid];
- const dialogMap: Record<string, (p: SolveEntryProps) => h.JSX.Element> = {
- question: SolveQuestionEntry,
- sms: SolveSmsEntry,
- email: SolveEmailEntry,
- post: SolvePostEntry,
- };
- const SolveDialog =
- selectedChallenge === undefined
- ? SolveUndefinedEntry
- : dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
-
- async function onNext(): Promise<void> {
- return reducer?.transition("solve_challenge", { answer });
- }
- function onCancel(): void {
- reducer?.back();
- }
+ const selectedChallenge = chArr.find((ch) => ch.uuid === selectedUuid);
- return (
- <AnastasisClientFrame hideNav title="Recovery: Solve challenge">
- <SolveOverviewFeedbackDisplay
- feedback={challengeFeedback[selectedUuid]}
- />
- <SolveDialog
- id={selectedUuid}
- answer={answer}
- setAnswer={setAnswer}
- challenge={selectedChallenge}
- feedback={challengeFeedback[selectedUuid]}
- />
-
- <div
- style={{
- marginTop: "2em",
- display: "flex",
- justifyContent: "space-between",
- }}
- >
- <button class="button" onClick={onCancel}>
- Cancel
- </button>
- <AsyncButton class="button is-info" onClick={onNext}>
- Confirm
- </AsyncButton>
- </div>
- </AnastasisClientFrame>
- );
-}
-
-export interface SolveEntryProps {
- id: string;
- challenge: ChallengeInfo;
- feedback?: ChallengeFeedback;
- answer: string;
- setAnswer: (s: string) => void;
-}
-
-function SolveSmsEntry({
- challenge,
- answer,
- setAnswer,
-}: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>
- An sms has been sent to "<b>{challenge.instructions}</b>". Type the code
- below
- </p>
- <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </Fragment>
- );
-}
-function SolveQuestionEntry({
- challenge,
- answer,
- setAnswer,
-}: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>Type the answer to the following question:</p>
- <pre>{challenge.instructions}</pre>
- <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </Fragment>
- );
-}
-
-function SolvePostEntry({
- challenge,
- answer,
- setAnswer,
-}: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>
- instruction for post type challenge "<b>{challenge.instructions}</b>"
- </p>
- <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </Fragment>
- );
-}
-
-function SolveEmailEntry({
- challenge,
- answer,
- setAnswer,
-}: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>
- An email has been sent to "<b>{challenge.instructions}</b>". Type the
- code below
- </p>
- <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </Fragment>
- );
-}
+ const SolveDialog =
+ !selectedChallenge ||
+ !authMethods[selectedChallenge.type as KnownAuthMethods]
+ ? SolveNotImplemented
+ : authMethods[selectedChallenge.type as KnownAuthMethods].solve ??
+ SolveNotImplemented;
-function SolveUnsupportedEntry(props: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>
- The challenge selected is not supported for this UI. Please update this
- version or try using another policy.
- </p>
- <p>
- <b>Challenge type:</b> {props.challenge.type}
- </p>
- </Fragment>
- );
-}
-function SolveUndefinedEntry(props: SolveEntryProps): VNode {
- return (
- <Fragment>
- <p>
- There is no challenge information for id <b>"{props.id}"</b>. Try
- resetting the recovery session.
- </p>
- </Fragment>
- );
+ return <SolveDialog id={selectedUuid} />;
}
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
index 657a2dd74..fcddaf87a 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.stories.tsx
@@ -15,24 +15,26 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../utils';
-import { StartScreen as TestedComponent } from './StartScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../utils";
+import { StartScreen as TestedComponent } from "./StartScreen";
export default {
- title: 'Pages/StartScreen',
+ title: "Pages/Start",
component: TestedComponent,
args: {
order: 1,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const InitialState = createExample(TestedComponent, reducerStatesExample.initial); \ No newline at end of file
+export const InitialState = createExample(
+ TestedComponent,
+ reducerStatesExample.initial,
+);
diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.tsx
index d53df4cae..8b24ef684 100644
--- a/packages/anastasis-webui/src/pages/home/StartScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/StartScreen.tsx
@@ -1,27 +1,36 @@
-
import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function StartScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
return (
<AnastasisClientFrame hideNav title="Home">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
-
<div class="buttons">
- <button class="button is-success" autoFocus onClick={() => reducer.startBackup()}>
- <div class="icon"><i class="mdi mdi-arrow-up" /></div>
+ <button
+ class="button is-success"
+ autoFocus
+ onClick={() => reducer.startBackup()}
+ >
+ <div class="icon">
+ <i class="mdi mdi-arrow-up" />
+ </div>
<span>Backup a secret</span>
</button>
- <button class="button is-info" onClick={() => reducer.startRecover()}>
- <div class="icon"><i class="mdi mdi-arrow-down" /></div>
+ <button
+ class="button is-info"
+ onClick={() => reducer.startRecover()}
+ >
+ <div class="icon">
+ <i class="mdi mdi-arrow-down" />
+ </div>
<span>Recover a secret</span>
</button>
@@ -30,7 +39,6 @@ export function StartScreen(): VNode {
<span>Restore a session</span>
</button> */}
</div>
-
</div>
<div class="column" />
</div>
diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
index 7568ccd69..245ad8889 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.stories.tsx
@@ -15,29 +15,31 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { ReducerState } from 'anastasis-core';
-import { createExample, reducerStatesExample } from '../../utils';
-import { TruthsPayingScreen as TestedComponent } from './TruthsPayingScreen';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../utils";
+import { TruthsPayingScreen as TestedComponent } from "./TruthsPayingScreen";
export default {
- title: 'Pages/backup/__TruthsPayingScreen',
+ title: "Pages/backup/__TruthsPaying",
component: TestedComponent,
args: {
order: 10,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-export const Example = createExample(TestedComponent, reducerStatesExample.truthsPaying);
+export const Example = createExample(
+ TestedComponent,
+ reducerStatesExample.truthsPaying,
+);
export const WithPaytoList = createExample(TestedComponent, {
...reducerStatesExample.truthsPaying,
- payments: ['payto://x-taler-bank/bank/account']
+ payments: ["payto://x-taler-bank/bank/account"],
} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
index 0b32e0db5..6f95fa93b 100644
--- a/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/TruthsPayingScreen.tsx
@@ -3,19 +3,19 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function TruthsPayingScreen(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
- return <div>no reducer in context</div>
+ return <div>no reducer in context</div>;
}
- if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
- return <div>invalid state</div>
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.backup_state === undefined
+ ) {
+ return <div>invalid state</div>;
}
const payments = reducer.currentReducerState.payments ?? [];
return (
- <AnastasisClientFrame
- hideNext={"FIXME"}
- title="Backup: Truths Paying"
- >
+ <AnastasisClientFrame hideNext={"FIXME"} title="Backup: Truths Paying">
<p>
Some of the providers require a payment to store the encrypted
authentication information.
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
index e178a4955..080a7ab31 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,51 +15,67 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/email',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/email",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'email'
+const type: KnownAuthMethods = "email";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [],
+ },
+);
-export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Email to sebasjm@email.com ',
- remove: () => null
- }]
-});
+export const WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Email to sebasjm@email.com ",
+ remove: () => null,
+ },
+ ],
+ },
+);
-export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Email to sebasjm@email.com',
- remove: () => null
- },{
- challenge: 'qwe',
- type,
- instructions: 'Email to someone@sebasjm.com',
- remove: () => null
- }]
-});
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Email to sebasjm@email.com",
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Email to someone@sebasjm.com",
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx
index 1a6be1b61..556e3bdbf 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSetup.tsx
@@ -1,59 +1,94 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
-import { AnastasisClientFrame } from "../index";
-import { TextInput } from "../../../components/fields/TextInput";
import { EmailInput } from "../../../components/fields/EmailInput";
+import { AnastasisClientFrame } from "../index";
+import { AuthMethodSetupProps } from "./index";
-const EMAIL_PATTERN = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+const EMAIL_PATTERN = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-export function AuthMethodEmailSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodEmailSetup({
+ cancel,
+ addAuthMethod,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [email, setEmail] = useState("");
- const addEmailAuth = (): void => addAuthMethod({
- authentication_method: {
- type: "email",
- instructions: `Email to ${email}`,
- challenge: encodeCrock(stringToBytes(email)),
- },
- });
- const emailError = !EMAIL_PATTERN.test(email) ? 'Email address is not valid' : undefined
- const errors = !email ? 'Add your email' : emailError
+ const addEmailAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "email",
+ instructions: `Email to ${email}`,
+ challenge: encodeCrock(stringToBytes(email)),
+ },
+ });
+ const emailError = !EMAIL_PATTERN.test(email)
+ ? "Email address is not valid"
+ : undefined;
+ const errors = !email ? "Add your email" : emailError;
+ function goNextIfNoErrors(): void {
+ if (!errors) addEmailAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<p>
For email authentication, you need to provide an email address. When
recovering your secret, you will need to enter the code you receive by
- email.
+ email. Add the uuid from the challenge
</p>
<div>
<EmailInput
label="Email address"
error={emailError}
+ onConfirm={goNextIfNoErrors}
placeholder="email@domain.com"
- bind={[email, setEmail]} />
+ bind={[email, setEmail]}
+ />
</div>
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your emails:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
- </div>
- })}
- </div></section>}
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your emails:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginBottom: "auto", marginTop: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
<div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addEmailAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addEmailAuth}
+ >
+ Add
+ </button>
</span>
</div>
</div>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
new file mode 100644
index 000000000..729fa8a1b
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.stories.tsx
@@ -0,0 +1,90 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/email",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "email";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "Email to me@domain.com",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
+
+export const PaymentFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "Email to me@domain.com",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ challenge_feedback: {
+ "uuid-1": {
+ state: ChallengeFeedbackStatus.Payment,
+ taler_pay_uri: "taler://pay/...",
+ provider: "https://localhost:8080/",
+ payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
+ },
+ },
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
new file mode 100644
index 000000000..e50c3bb20
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodEmailSolve.tsx
@@ -0,0 +1,148 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("A-");
+ const [expanded, setExpanded] = useState(false);
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="Email challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>
+ An email has been sent to "<b>{selectedChallenge.instructions}</b>". The
+ message has and identification code and recovery code that starts with "
+ <b>A-</b>". Wait the message to arrive and the enter the recovery code
+ below.
+ </p>
+ {!expanded ? (
+ <p>
+ The identification code in the email should start with "
+ {selectedUuid.substring(0, 10)}"
+ <span
+ class="icon has-tooltip-top"
+ data-tooltip="click to expand"
+ onClick={() => setExpanded((e) => !e)}
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ </p>
+ ) : (
+ <p>
+ The identification code in the email is "{selectedUuid}"
+ <span
+ class="icon has-tooltip-top"
+ data-tooltip="click to show less code"
+ onClick={() => setExpanded((e) => !e)}
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ </p>
+ )}
+ <TextInput
+ label="Answer"
+ grabFocus
+ onConfirm={onNext}
+ bind={[answer, setAnswer]}
+ placeholder="A-1234567812345678"
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
index 71f618646..c521e18fd 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.stories.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@@ -16,50 +15,66 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/IBAN',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/IBAN",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'iban'
+const type: KnownAuthMethods = "iban";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [],
+ },
+);
-export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Wire transfer from QWEASD123123 with holder Sebastian',
- remove: () => null
- }]
-});
-export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Wire transfer from QWEASD123123 with holder Javier',
- remove: () => null
- },{
- challenge: 'qwe',
- type,
- instructions: 'Wire transfer from QWEASD123123 with holder Sebastian',
- remove: () => null
- }]
-},);
+export const WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Wire transfer from QWEASD123123 with holder Sebastian",
+ remove: () => null,
+ },
+ ],
+ },
+);
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Wire transfer from QWEASD123123 with holder Javier",
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Wire transfer from QWEASD123123 with holder Sebastian",
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx
index c9edbfa07..501a40600 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSetup.tsx
@@ -1,65 +1,111 @@
-/* eslint-disable @typescript-eslint/camelcase */
import {
canonicalJson,
encodeCrock,
- stringToBytes
+ stringToBytes,
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
+import { AuthMethodSetupProps } from ".";
import { TextInput } from "../../../components/fields/TextInput";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
import { AnastasisClientFrame } from "../index";
-export function AuthMethodIbanSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodIbanSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [name, setName] = useState("");
const [account, setAccount] = useState("");
- const addIbanAuth = (): void => addAuthMethod({
- authentication_method: {
- type: "iban",
- instructions: `Wire transfer from ${account} with holder ${name}`,
- challenge: encodeCrock(stringToBytes(canonicalJson({
- name, account
- }))),
- },
- });
- const errors = !name ? 'Add an account name' : (
- !account ? 'Add an account IBAN number' : undefined
- )
+ const addIbanAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "iban",
+ instructions: `Wire transfer from ${account} with holder ${name}`,
+ challenge: encodeCrock(
+ stringToBytes(
+ canonicalJson({
+ name,
+ account,
+ }),
+ ),
+ ),
+ },
+ });
+ const errors = !name
+ ? "Add an account name"
+ : !account
+ ? "Add an account IBAN number"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addIbanAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add bank transfer authentication">
<p>
- For bank transfer authentication, you need to provide a bank
- account (account holder name and IBAN). When recovering your
- secret, you will be asked to pay the recovery fee via bank
- transfer from the account you provided here.
+ For bank transfer authentication, you need to provide a bank account
+ (account holder name and IBAN). When recovering your secret, you will be
+ asked to pay the recovery fee via bank transfer from the account you
+ provided here.
</p>
<div>
<TextInput
label="Bank account holder name"
grabFocus
placeholder="John Smith"
- bind={[name, setName]} />
+ onConfirm={goNextIfNoErrors}
+ bind={[name, setName]}
+ />
<TextInput
label="IBAN"
placeholder="DE91100000000123456789"
- bind={[account, setAccount]} />
+ onConfirm={goNextIfNoErrors}
+ bind={[account, setAccount]}
+ />
</div>
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your bank accounts:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
- </div>
- })}
- </div></section>}
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your bank accounts:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginBottom: "auto", marginTop: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
<div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addIbanAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addIbanAuth}
+ >
+ Add
+ </button>
</span>
</div>
</div>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
new file mode 100644
index 000000000..cbbc253e9
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/Iban",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "iban";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.tsx
new file mode 100644
index 000000000..5cff7bf01
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodIbanSolve.tsx
@@ -0,0 +1,112 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="IBAN Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>Send a wire transfer to the address,</p>
+ <button class="button">Check</button>
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
index 0f1c17495..2977586ac 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.stories.tsx
@@ -16,51 +16,67 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/Post',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/Post",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'post'
+const type: KnownAuthMethods = "post";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [],
+ },
+);
-export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Letter to address in postal code QWE456',
- remove: () => null
- }]
-});
+export const WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Letter to address in postal code QWE456",
+ remove: () => null,
+ },
+ ],
+ },
+);
-export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Letter to address in postal code QWE456',
- remove: () => null
- },{
- challenge: 'qwe',
- type,
- instructions: 'Letter to address in postal code ABC123',
- remove: () => null
- }]
-});
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Letter to address in postal code QWE456",
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Letter to address in postal code ABC123",
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx
index bfeaaa832..04e00500c 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSetup.tsx
@@ -1,15 +1,19 @@
-/* eslint-disable @typescript-eslint/camelcase */
import {
- canonicalJson, encodeCrock,
- stringToBytes
+ canonicalJson,
+ encodeCrock,
+ stringToBytes,
} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
-import { TextInput } from "../../../components/fields/TextInput";
import { AnastasisClientFrame } from "..";
+import { TextInput } from "../../../components/fields/TextInput";
+import { AuthMethodSetupProps } from "./index";
-export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodPostSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [fullName, setFullName] = useState("");
const [street, setStreet] = useState("");
const [city, setCity] = useState("");
@@ -33,68 +37,108 @@ export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthM
});
};
- const errors = !fullName ? 'The full name is missing' : (
- !street ? 'The street is missing' : (
- !city ? 'The city is missing' : (
- !postcode ? 'The postcode is missing' : (
- !country ? 'The country is missing' : undefined
- )
- )
- )
- )
+ const errors = !fullName
+ ? "The full name is missing"
+ : !street
+ ? "The street is missing"
+ : !city
+ ? "The city is missing"
+ : !postcode
+ ? "The postcode is missing"
+ : !country
+ ? "The country is missing"
+ : undefined;
+
+ function goNextIfNoErrors(): void {
+ if (!errors) addPostAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add postal authentication">
<p>
- For postal letter authentication, you need to provide a postal
- address. When recovering your secret, you will be asked to enter a
- code that you will receive in a letter to that address.
+ For postal letter authentication, you need to provide a postal address.
+ When recovering your secret, you will be asked to enter a code that you
+ will receive in a letter to that address.
</p>
<div>
<TextInput
grabFocus
label="Full Name"
bind={[fullName, setFullName]}
+ onConfirm={goNextIfNoErrors}
/>
</div>
<div>
<TextInput
+ onConfirm={goNextIfNoErrors}
label="Street"
bind={[street, setStreet]}
/>
</div>
<div>
<TextInput
- label="City" bind={[city, setCity]}
+ onConfirm={goNextIfNoErrors}
+ label="City"
+ bind={[city, setCity]}
/>
</div>
<div>
<TextInput
- label="Postal Code" bind={[postcode, setPostcode]}
+ onConfirm={goNextIfNoErrors}
+ label="Postal Code"
+ bind={[postcode, setPostcode]}
/>
</div>
<div>
<TextInput
+ onConfirm={goNextIfNoErrors}
label="Country"
bind={[country, setCountry]}
/>
</div>
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your postal code:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
- </div>
- })}
- </div>
- </section>}
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your postal code:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginBottom: "auto", marginTop: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addPostAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addPostAuth}
+ >
+ Add
+ </button>
</span>
</div>
</AnastasisClientFrame>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
new file mode 100644
index 000000000..3b67ee884
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/post",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "post";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
new file mode 100644
index 000000000..1bbbbfc03
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodPostSolve.tsx
@@ -0,0 +1,117 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("A-");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="Postal Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>Wait for the answer</p>
+ <TextInput
+ onConfirm={onNext}
+ label="Answer"
+ grabFocus
+ bind={[answer, setAnswer]}
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
index 3ba4a84ca..991301cbf 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.stories.tsx
@@ -16,51 +16,69 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/Question',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/Question",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'question'
+const type: KnownAuthMethods = "question";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [],
+ },
+);
-export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Is integer factorization polynomial? (non-quantum computer)',
- remove: () => null
- }]
-});
+export const WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions:
+ "Is integer factorization polynomial? (non-quantum computer)",
+ remove: () => null,
+ },
+ ],
+ },
+);
-export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Does P equal NP?',
- remove: () => null
- },{
- challenge: 'asd',
- type,
- instructions: 'Are continuous groups automatically differential groups?',
- remove: () => null
- }]
-});
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "Does P equal NP?",
+ remove: () => null,
+ },
+ {
+ challenge: "asd",
+ type,
+ instructions:
+ "Are continuous groups automatically differential groups?",
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx
index 04fa00d59..19260c4ff 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSetup.tsx
@@ -1,33 +1,39 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index";
import { TextInput } from "../../../components/fields/TextInput";
-export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodQuestionSetup({
+ cancel,
+ addAuthMethod,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [questionText, setQuestionText] = useState("");
const [answerText, setAnswerText] = useState("");
- const addQuestionAuth = (): void => addAuthMethod({
- authentication_method: {
- type: "question",
- instructions: questionText,
- challenge: encodeCrock(stringToBytes(answerText)),
- },
- });
+ const addQuestionAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "question",
+ instructions: questionText,
+ challenge: encodeCrock(stringToBytes(answerText)),
+ },
+ });
- const errors = !questionText ? "Add your security question" : (
- !answerText ? 'Add the answer to your question' : undefined
- )
+ const errors = !questionText
+ ? "Add your security question"
+ : !answerText
+ ? "Add the answer to your question"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addQuestionAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add Security Question">
<div>
<p>
- For2 security question authentication, you need to provide a question
+ For security question authentication, you need to provide a question
and its answer. When recovering your secret, you will be shown the
question and you will need to type the answer exactly as you typed it
here.
@@ -36,36 +42,67 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
<TextInput
label="Security question"
grabFocus
+ onConfirm={goNextIfNoErrors}
placeholder="Your question"
- bind={[questionText, setQuestionText]} />
+ bind={[questionText, setQuestionText]}
+ />
</div>
<div>
<TextInput
label="Answer"
+ onConfirm={goNextIfNoErrors}
placeholder="Your answer"
bind={[answerText, setAnswerText]}
/>
</div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addQuestionAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addQuestionAuth}
+ >
+ Add
+ </button>
</span>
</div>
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your security questions:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove} >Delete</button></div>
- </div>
- })}
- </div></section>}
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your security questions:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginBottom: "auto", marginTop: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
</div>
- </AnastasisClientFrame >
+ </AnastasisClientFrame>
);
}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
new file mode 100644
index 000000000..1fa9fd6ec
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.stories.tsx
@@ -0,0 +1,258 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/question",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "question";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
+
+export const MessageFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Message,
+ message: "Challenge should be solved",
+ },
+ },
+} as ReducerState);
+
+export const ServerFailureFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.ServerFailure,
+ http_status: 500,
+ error_response: "Couldn't connect to mysql",
+ },
+ },
+ } as ReducerState,
+);
+
+export const RedirectFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Redirect,
+ http_status: 302,
+ redirect_url: "http://video.taler.net",
+ },
+ },
+} as ReducerState);
+
+export const MessageRateLimitExceededFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.RateLimitExceeded,
+ },
+ },
+ } as ReducerState,
+);
+
+export const UnsupportedFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Unsupported,
+ http_status: 500,
+ unsupported_method: "Question",
+ },
+ },
+} as ReducerState);
+
+export const TruthUnknownFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.TruthUnknown,
+ },
+ },
+} as ReducerState);
+
+export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.AuthIban,
+ challenge_amount: "EUR:1",
+ credit_iban: "DE12345789000",
+ business_name: "Data Loss Incorporated",
+ wire_transfer_subject: "Anastasis 987654321",
+ answer_code: 987654321,
+ // Fields that follow are only for compatibility with C reducer,
+ // will be removed eventually,
+ details: {
+ business_name: "foo",
+ challenge_amount: "foo",
+ credit_iban: "foo",
+ wire_transfer_subject: "foo",
+ },
+ method: "iban",
+ },
+ },
+} as ReducerState);
+
+export const PaymentFeedback = createExample(TestedComponent[type].solve, {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "ASDASDSAD!1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "ASDASDSAD!1",
+ challenge_feedback: {
+ "ASDASDSAD!1": {
+ state: ChallengeFeedbackStatus.Payment,
+ taler_pay_uri: "taler://pay/...",
+ provider: "https://localhost:8080/",
+ payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
+ },
+ },
+} as ReducerState);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.tsx
new file mode 100644
index 000000000..2636ca47c
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodQuestionSolve.tsx
@@ -0,0 +1,121 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="Question challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>
+ In this challenge you need to provide the answer for the next question:
+ </p>
+ <pre>{selectedChallenge.instructions}</pre>
+ <p>Type the answer below</p>
+ <TextInput
+ label="Answer"
+ onConfirm={onNext}
+ grabFocus
+ bind={[answer, setAnswer]}
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
index ae8297ef7..3a44c7ad0 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.stories.tsx
@@ -16,51 +16,67 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/Sms',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/Sms",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'sms'
+const type: KnownAuthMethods = "sms";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [],
+ },
+);
-export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'SMS to +11-1234-2345',
- remove: () => null
- }]
-});
+export const WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "SMS to +11-1234-2345",
+ remove: () => null,
+ },
+ ],
+ },
+);
-export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'SMS to +11-1234-2345',
- remove: () => null
- },{
- challenge: 'qwe',
- type,
- instructions: 'SMS to +11-5555-2345',
- remove: () => null
- }]
-});
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: "SMS to +11-1234-2345",
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: "SMS to +11-5555-2345",
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx
index 9e85af2b2..e70b2a53b 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSetup.tsx
@@ -1,15 +1,15 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
-import { NumberInput } from "../../../components/fields/NumberInput";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AuthMethodSetupProps } from ".";
+import { PhoneNumberInput } from "../../../components/fields/NumberInput";
import { AnastasisClientFrame } from "../index";
-export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodSmsSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [mobileNumber, setMobileNumber] = useState("");
const addSmsAuth = (): void => {
addAuthMethod({
@@ -24,7 +24,10 @@ export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMe
useLayoutEffect(() => {
inputRef.current?.focus();
}, []);
- const errors = !mobileNumber ? 'Add a mobile number' : undefined
+ const errors = !mobileNumber ? "Add a mobile number" : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addSmsAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add SMS authentication">
<div>
@@ -34,27 +37,57 @@ export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMe
receive via SMS.
</p>
<div class="container">
- <NumberInput
+ <PhoneNumberInput
label="Mobile number"
placeholder="Your mobile number"
+ onConfirm={goNextIfNoErrors}
grabFocus
- bind={[mobileNumber, setMobileNumber]} />
+ bind={[mobileNumber, setMobileNumber]}
+ />
</div>
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your mobile numbers:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginTop: 'auto', marginBottom: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove}>Delete</button></div>
- </div>
- })}
- </div></section>}
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your mobile numbers:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginTop: "auto", marginBottom: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addSmsAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addSmsAuth}
+ >
+ Add
+ </button>
</span>
</div>
</div>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
new file mode 100644
index 000000000..e8961cccf
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/sms",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "sms";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "SMS to +54 11 2233 4455",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
new file mode 100644
index 000000000..3370c76d0
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodSmsSolve.tsx
@@ -0,0 +1,148 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("A-");
+
+ const [expanded, setExpanded] = useState(false);
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="SMS Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>
+ An sms has been sent to "<b>{selectedChallenge.instructions}</b>". The
+ message has and identification code and recovery code that starts with "
+ <b>A-</b>". Wait the message to arrive and the enter the recovery code
+ below.
+ </p>
+ {!expanded ? (
+ <p>
+ The identification code in the SMS should start with "
+ {selectedUuid.substring(0, 10)}"
+ <span
+ class="icon has-tooltip-top"
+ data-tooltip="click to expand"
+ onClick={() => setExpanded((e) => !e)}
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ </p>
+ ) : (
+ <p>
+ The identification code in the SMS is "{selectedUuid}"
+ <span
+ class="icon has-tooltip-top"
+ data-tooltip="click to show less code"
+ onClick={() => setExpanded((e) => !e)}
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ </p>
+ )}
+ <TextInput
+ label="Answer"
+ grabFocus
+ onConfirm={onNext}
+ bind={[answer, setAnswer]}
+ placeholder="A-1234567812345678"
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
index 4e46b600e..bc4628828 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.stories.tsx
@@ -16,49 +16,65 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
- title: 'Pages/backup/authMethods/TOTP',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/TOTP",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'totp'
+const type: KnownAuthMethods = "totp";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
-export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Enter 8 digits code for "Anastasis"',
- remove: () => null
- }]
-});
-export const WithMoreExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: 'Enter 8 digits code for "Anastasis1"',
- remove: () => null
- },{
- challenge: 'qwe',
- type,
- instructions: 'Enter 8 digits code for "Anastasis2"',
- remove: () => null
- }]
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [],
+ },
+);
+export const WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: 'Enter 8 digits code for "Anastasis"',
+ remove: () => null,
+ },
+ ],
+ },
+);
+export const WithMoreExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: 'Enter 8 digits code for "Anastasis1"',
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: 'Enter 8 digits code for "Anastasis2"',
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
index fd0bd0224..6b0dd7a79 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
@@ -1,40 +1,46 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useMemo, useState } from "preact/hooks";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index";
import { TextInput } from "../../../components/fields/TextInput";
import { QR } from "../../../components/QR";
import { base32enc, computeTOTPandCheck } from "./totp";
-export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
+export function AuthMethodTotpSetup({
+ addAuthMethod,
+ cancel,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [name, setName] = useState("anastasis");
const [test, setTest] = useState("");
- const digits = 8
+ const digits = 8;
const secretKey = useMemo(() => {
- const array = new Uint8Array(32)
- return window.crypto.getRandomValues(array)
- }, [])
+ const array = new Uint8Array(32);
+ return window.crypto.getRandomValues(array);
+ }, []);
const secret32 = base32enc(secretKey);
- const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`
+ const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`;
- const addTotpAuth = (): void => addAuthMethod({
- authentication_method: {
- type: "totp",
- instructions: `Enter ${digits} digits code for "${name}"`,
- challenge: encodeCrock(stringToBytes(totpURL)),
- },
- });
+ const addTotpAuth = (): void =>
+ addAuthMethod({
+ authentication_method: {
+ type: "totp",
+ instructions: `Enter ${digits} digits code for "${name}"`,
+ challenge: encodeCrock(stringToBytes(totpURL)),
+ },
+ });
const testCodeMatches = computeTOTPandCheck(secretKey, 8, parseInt(test, 10));
- const errors = !name ? 'The TOTP name is missing' : (
- !testCodeMatches ? 'The test code doesnt match' : undefined
- );
+ const errors = !name
+ ? "The TOTP name is missing"
+ : !testCodeMatches
+ ? "The test code doesnt match"
+ : undefined;
+ function goNextIfNoErrors(): void {
+ if (!errors) addTotpAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add TOTP authentication">
<p>
@@ -43,10 +49,7 @@ export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthM
with your TOTP App to import the TOTP secret into your TOTP App.
</p>
<div class="block">
- <TextInput
- label="TOTP Name"
- grabFocus
- bind={[name, setName]} />
+ <TextInput label="TOTP Name" grabFocus bind={[name, setName]} />
</div>
<div style={{ height: 300 }}>
<QR text={totpURL} />
@@ -56,23 +59,53 @@ export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthM
</p>
<TextInput
label="Test code"
- bind={[test, setTest]} />
- {configured.length > 0 && <section class="section">
- <div class="block">
- Your TOTP numbers:
- </div><div class="block">
- {configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <p style={{ marginTop: 'auto', marginBottom: 'auto' }}>{c.instructions}</p>
- <div><button class="button is-danger" onClick={c.remove}>Delete</button></div>
- </div>
- })}
- </div></section>}
+ onConfirm={goNextIfNoErrors}
+ bind={[test, setTest]}
+ />
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your TOTP numbers:</div>
+ <div class="block">
+ {configured.map((c, i) => {
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <p style={{ marginTop: "auto", marginBottom: "auto" }}>
+ {c.instructions}
+ </p>
+ <div>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ </section>
+ )}
<div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={errors !== undefined} onClick={addTotpAuth}>Add</button>
+ <button
+ class="button is-info"
+ disabled={errors !== undefined}
+ onClick={addTotpAuth}
+ >
+ Add
+ </button>
</span>
</div>
</div>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
new file mode 100644
index 000000000..8743c5a73
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/totp",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "totp";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.tsx
new file mode 100644
index 000000000..347f9bf03
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSolve.tsx
@@ -0,0 +1,118 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="TOTP Challenge">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>enter the totp solution</p>
+ <TextInput
+ label="Answer"
+ onConfirm={onNext}
+ grabFocus
+ bind={[answer, setAnswer]}
+ />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
+// NKE8 VD857T X033X6RG WEGPYP6D70 Q7YE XN8D2 ZN79SCN 231B4QK0
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx
index 3c4c7bf39..4aad0a097 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.stories.tsx
@@ -16,51 +16,68 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample, reducerStatesExample } from '../../../utils';
-import { authMethods as TestedComponent, KnownAuthMethods } from './index';
-import logoImage from '../../../assets/logo.jpeg'
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+import logoImage from "../../../assets/logo.jpeg";
export default {
- title: 'Pages/backup/authMethods/Video',
+ title: "Pages/backup/AuthorizationMethod/AuthMethods/Video",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
- onUpdate: { action: 'onUpdate' },
- onBack: { action: 'onBack' },
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
},
};
-const type: KnownAuthMethods = 'video'
+const type: KnownAuthMethods = "video";
-export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: []
-});
+export const Empty = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [],
+ },
+);
-export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: logoImage,
- remove: () => null
- }]
-});
+export const WithOneExample = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: logoImage,
+ remove: () => null,
+ },
+ ],
+ },
+);
-export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, {
- configured: [{
- challenge: 'qwe',
- type,
- instructions: logoImage,
- remove: () => null
- },{
- challenge: 'qwe',
- type,
- instructions: logoImage,
- remove: () => null
- }]
-});
+export const WithMoreExamples = createExample(
+ TestedComponent[type].setup,
+ reducerStatesExample.authEditing,
+ {
+ configured: [
+ {
+ challenge: "qwe",
+ type,
+ instructions: logoImage,
+ remove: () => null,
+ },
+ {
+ challenge: "qwe",
+ type,
+ instructions: logoImage,
+ remove: () => null,
+ },
+ ],
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx
index 8be999b3f..04a129c4a 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSetup.tsx
@@ -1,54 +1,90 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import {
- encodeCrock,
- stringToBytes
-} from "@gnu-taler/taler-util";
+import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ImageInput } from "../../../components/fields/ImageInput";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index";
-export function AuthMethodVideoSetup({cancel, addAuthMethod, configured}: AuthMethodSetupProps): VNode {
+export function AuthMethodVideoSetup({
+ cancel,
+ addAuthMethod,
+ configured,
+}: AuthMethodSetupProps): VNode {
const [image, setImage] = useState("");
const addVideoAuth = (): void => {
addAuthMethod({
authentication_method: {
type: "video",
- instructions: 'Join a video call',
+ instructions: "Join a video call",
challenge: encodeCrock(stringToBytes(image)),
},
- })
+ });
};
+ function goNextIfNoErrors(): void {
+ addVideoAuth();
+ }
return (
<AnastasisClientFrame hideNav title="Add video authentication">
<p>
- For video identification, you need to provide a passport-style
- photograph. When recovering your secret, you will be asked to join a
- video call. During that call, a human will use the photograph to
- verify your identity.
+ For video identification, you need to provide a passport-style
+ photograph. When recovering your secret, you will be asked to join a
+ video call. During that call, a human will use the photograph to verify
+ your identity.
</p>
- <div style={{textAlign:'center'}}>
+ <div style={{ textAlign: "center" }}>
<ImageInput
label="Choose photograph"
grabFocus
- bind={[image, setImage]} />
+ onConfirm={goNextIfNoErrors}
+ bind={[image, setImage]}
+ />
</div>
- {configured.length > 0 && <section class="section">
+ {configured.length > 0 && (
+ <section class="section">
+ <div class="block">Your photographs:</div>
<div class="block">
- Your photographs:
- </div><div class="block">
{configured.map((c, i) => {
- return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
- <img style={{ marginTop: 'auto', marginBottom: 'auto', width: 100, height:100, border: 'solid 1px black' }} src={c.instructions} />
- <div style={{marginTop: 'auto', marginBottom: 'auto'}}><button class="button is-danger" onClick={c.remove}>Delete</button></div>
- </div>
+ return (
+ <div
+ key={i}
+ class="box"
+ style={{ display: "flex", justifyContent: "space-between" }}
+ >
+ <img
+ style={{
+ marginTop: "auto",
+ marginBottom: "auto",
+ width: 100,
+ height: 100,
+ border: "solid 1px black",
+ }}
+ src={c.instructions}
+ />
+ <div style={{ marginTop: "auto", marginBottom: "auto" }}>
+ <button class="button is-danger" onClick={c.remove}>
+ Delete
+ </button>
+ </div>
+ </div>
+ );
})}
- </div></section>}
+ </div>
+ </section>
+ )}
<div>
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
- <button class="button is-info" onClick={addVideoAuth}>Add</button>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={cancel}>
+ Cancel
+ </button>
+ <button class="button is-info" onClick={addVideoAuth}>
+ Add
+ </button>
</div>
</div>
</AnastasisClientFrame>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.stories.tsx
new file mode 100644
index 000000000..7c5511c5a
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.stories.tsx
@@ -0,0 +1,60 @@
+/*
+ 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 { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
+import { createExample, reducerStatesExample } from "../../../utils";
+import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
+
+export default {
+ title: "Pages/recovery/SolveChallenge/AuthMethods/video",
+ component: TestedComponent,
+ args: {
+ order: 5,
+ },
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
+
+const type: KnownAuthMethods = "video";
+
+export const WithoutFeedback = createExample(
+ TestedComponent[type].solve,
+ {
+ ...reducerStatesExample.challengeSolving,
+ recovery_information: {
+ challenges: [
+ {
+ cost: "USD:1",
+ instructions: "does P equals NP?",
+ type: "question",
+ uuid: "uuid-1",
+ },
+ ],
+ policies: [],
+ },
+ selected_challenge_uuid: "uuid-1",
+ } as ReducerState,
+ {
+ id: "uuid-1",
+ },
+);
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.tsx
new file mode 100644
index 000000000..efadb9a9a
--- /dev/null
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodVideoSolve.tsx
@@ -0,0 +1,112 @@
+import { ChallengeFeedbackStatus, ChallengeInfo } from "anastasis-core";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../components/AsyncButton";
+import { TextInput } from "../../../components/fields/TextInput";
+import { useAnastasisContext } from "../../../context/anastasis";
+import { AnastasisClientFrame } from "../index";
+import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
+import { AuthMethodSolveProps } from "./index";
+
+export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
+ const [answer, setAnswer] = useState("");
+
+ const reducer = useAnastasisContext();
+ if (!reducer) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>no reducer in context</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (
+ !reducer.currentReducerState ||
+ reducer.currentReducerState.recovery_state === undefined
+ ) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ if (!reducer.currentReducerState.recovery_information) {
+ return (
+ <AnastasisClientFrame
+ hideNext="Recovery document not found"
+ title="Recovery problem"
+ >
+ <div>no recovery information found</div>
+ </AnastasisClientFrame>
+ );
+ }
+ if (!reducer.currentReducerState.selected_challenge_uuid) {
+ return (
+ <AnastasisClientFrame hideNav title="Recovery problem">
+ <div>invalid state</div>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={() => reducer.back()}>
+ Back
+ </button>
+ </div>
+ </AnastasisClientFrame>
+ );
+ }
+
+ const chArr = reducer.currentReducerState.recovery_information.challenges;
+ const challengeFeedback =
+ reducer.currentReducerState.challenge_feedback ?? {};
+ const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
+ const challenges: {
+ [uuid: string]: ChallengeInfo;
+ } = {};
+ for (const ch of chArr) {
+ challenges[ch.uuid] = ch;
+ }
+ const selectedChallenge = challenges[selectedUuid];
+ const feedback = challengeFeedback[selectedUuid];
+
+ async function onNext(): Promise<void> {
+ return reducer?.transition("solve_challenge", { answer });
+ }
+ function onCancel(): void {
+ reducer?.back();
+ }
+
+ const shouldHideConfirm =
+ feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
+ feedback?.state === ChallengeFeedbackStatus.Redirect ||
+ feedback?.state === ChallengeFeedbackStatus.Unsupported ||
+ feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
+
+ return (
+ <AnastasisClientFrame hideNav title="Add email authentication">
+ <SolveOverviewFeedbackDisplay feedback={feedback} />
+ <p>You are gonna be called to check your identity</p>
+ <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
+
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button class="button" onClick={onCancel}>
+ Cancel
+ </button>
+ {!shouldHideConfirm && (
+ <AsyncButton class="button is-info" onClick={onNext}>
+ Confirm
+ </AsyncButton>
+ )}
+ </div>
+ </AnastasisClientFrame>
+ );
+}
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/index.tsx b/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
index 7b0cce883..b4f649488 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
@@ -1,25 +1,60 @@
+import { AuthMethod } from "anastasis-core";
import { h, VNode } from "preact";
-import { AuthMethodSetupProps } from "../AuthenticationEditorScreen";
+import postalIcon from "../../../assets/icons/auth_method/postal.svg";
+import questionIcon from "../../../assets/icons/auth_method/question.svg";
+import smsIcon from "../../../assets/icons/auth_method/sms.svg";
+import videoIcon from "../../../assets/icons/auth_method/video.svg";
+import { AuthMethodEmailSetup as EmailSetup } from "./AuthMethodEmailSetup";
+import { AuthMethodEmailSolve as EmailSolve } from "./AuthMethodEmailSolve";
+import { AuthMethodIbanSetup as IbanSetup } from "./AuthMethodIbanSetup";
+import { AuthMethodPostSetup as PostalSetup } from "./AuthMethodPostSetup";
+import { AuthMethodQuestionSetup as QuestionSetup } from "./AuthMethodQuestionSetup";
+import { AuthMethodSmsSetup as SmsSetup } from "./AuthMethodSmsSetup";
+import { AuthMethodTotpSetup as TotpSetup } from "./AuthMethodTotpSetup";
+import { AuthMethodVideoSetup as VideoSetup } from "./AuthMethodVideoSetup";
-import { AuthMethodEmailSetup as EmailScreen } from "./AuthMethodEmailSetup";
-import { AuthMethodIbanSetup as IbanScreen } from "./AuthMethodIbanSetup";
-import { AuthMethodPostSetup as PostalScreen } from "./AuthMethodPostSetup";
-import { AuthMethodQuestionSetup as QuestionScreen } from "./AuthMethodQuestionSetup";
-import { AuthMethodSmsSetup as SmsScreen } from "./AuthMethodSmsSetup";
-import { AuthMethodTotpSetup as TotpScreen } from "./AuthMethodTotpSetup";
-import { AuthMethodVideoSetup as VideScreen } from "./AuthMethodVideoSetup";
-import postalIcon from '../../../assets/icons/auth_method/postal.svg';
-import questionIcon from '../../../assets/icons/auth_method/question.svg';
-import smsIcon from '../../../assets/icons/auth_method/sms.svg';
-import videoIcon from '../../../assets/icons/auth_method/video.svg';
+import { AuthMethodIbanSolve as IbanSolve } from "./AuthMethodIbanSolve";
+import { AuthMethodPostSolve as PostalSolve } from "./AuthMethodPostSolve";
+import { AuthMethodQuestionSolve as QuestionSolve } from "./AuthMethodQuestionSolve";
+import { AuthMethodSmsSolve as SmsSolve } from "./AuthMethodSmsSolve";
+import { AuthMethodTotpSolve as TotpSolve } from "./AuthMethodTotpSolve";
+import { AuthMethodVideoSolve as VideoSolve } from "./AuthMethodVideoSolve";
+
+export type AuthMethodWithRemove = AuthMethod & { remove: () => void };
+
+export interface AuthMethodSetupProps {
+ method: string;
+ addAuthMethod: (x: any) => void;
+ configured: AuthMethodWithRemove[];
+ cancel: () => void;
+}
+
+export interface AuthMethodSolveProps {
+ id: string;
+}
interface AuthMethodConfiguration {
icon: VNode;
label: string;
- screen: (props: AuthMethodSetupProps) => VNode;
+ setup: (props: AuthMethodSetupProps) => VNode;
+ solve: (props: AuthMethodSolveProps) => VNode;
skip?: boolean;
}
-export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
+// export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
+
+const ALL_METHODS = [
+ "sms",
+ "email",
+ "post",
+ "question",
+ "video",
+ "totp",
+ "iban",
+] as const;
+export type KnownAuthMethods = typeof ALL_METHODS[number];
+export function isKnownAuthMethods(value: string): value is KnownAuthMethods {
+ return ALL_METHODS.includes(value as KnownAuthMethods);
+}
type KnowMethodConfig = {
[name in KnownAuthMethods]: AuthMethodConfiguration;
@@ -29,41 +64,44 @@ export const authMethods: KnowMethodConfig = {
question: {
icon: <img src={questionIcon} />,
label: "Question",
- screen: QuestionScreen
+ setup: QuestionSetup,
+ solve: QuestionSolve,
},
sms: {
icon: <img src={smsIcon} />,
label: "SMS",
- screen: SmsScreen
+ setup: SmsSetup,
+ solve: SmsSolve,
},
email: {
icon: <i class="mdi mdi-email" />,
label: "Email",
- screen: EmailScreen
-
+ setup: EmailSetup,
+ solve: EmailSolve,
},
iban: {
icon: <i class="mdi mdi-bank" />,
label: "IBAN",
- screen: IbanScreen
-
+ setup: IbanSetup,
+ solve: IbanSolve,
},
post: {
icon: <img src={postalIcon} />,
label: "Physical mail",
- screen: PostalScreen
-
+ setup: PostalSetup,
+ solve: PostalSolve,
},
totp: {
icon: <i class="mdi mdi-devices" />,
label: "TOTP",
- screen: TotpScreen
-
+ setup: TotpSetup,
+ solve: TotpSolve,
},
video: {
icon: <img src={videoIcon} />,
label: "Video",
- screen: VideScreen,
- skip: true,
- }
-} \ No newline at end of file
+ setup: VideoSetup,
+ solve: VideoSolve,
+ skip: true,
+ },
+};
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/totp.ts b/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
index 0bc3feaf8..c2288671c 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
+++ b/packages/anastasis-webui/src/pages/home/authMethod/totp.ts
@@ -1,54 +1,61 @@
/* eslint-disable @typescript-eslint/camelcase */
-import jssha from 'jssha'
+import jssha from "jssha";
-const SEARCH_RANGE = 16
-const timeStep = 30
+const SEARCH_RANGE = 16;
+const timeStep = 30;
-export function computeTOTPandCheck(secretKey: Uint8Array, digits: number, code: number): boolean {
- const now = new Date().getTime()
+export function computeTOTPandCheck(
+ secretKey: Uint8Array,
+ digits: number,
+ code: number,
+): boolean {
+ const now = new Date().getTime();
const epoch = Math.floor(Math.round(now / 1000.0) / timeStep);
for (let ms = -SEARCH_RANGE; ms < SEARCH_RANGE; ms++) {
const movingFactor = (epoch + ms).toString(16).padStart(16, "0");
- const hmacSha = new jssha('SHA-1', 'HEX', { hmacKey: { value: secretKey, format: 'UINT8ARRAY' } });
+ const hmacSha = new jssha("SHA-1", "HEX", {
+ hmacKey: { value: secretKey, format: "UINT8ARRAY" },
+ });
hmacSha.update(movingFactor);
- const hmac_text = hmacSha.getHMAC('UINT8ARRAY');
+ const hmac_text = hmacSha.getHMAC("UINT8ARRAY");
- const offset = (hmac_text[hmac_text.length - 1] & 0xf)
+ const offset = hmac_text[hmac_text.length - 1] & 0xf;
- const otp = ((
- (hmac_text[offset + 0] << 24) +
- (hmac_text[offset + 1] << 16) +
- (hmac_text[offset + 2] << 8) +
- (hmac_text[offset + 3])
- ) & 0x7fffffff) % Math.pow(10, digits)
+ const otp =
+ (((hmac_text[offset + 0] << 24) +
+ (hmac_text[offset + 1] << 16) +
+ (hmac_text[offset + 2] << 8) +
+ hmac_text[offset + 3]) &
+ 0x7fffffff) %
+ Math.pow(10, digits);
- if (otp == code) return true
+ if (otp == code) return true;
}
- return false
+ return false;
}
-const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split('')
+const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split("");
export function base32enc(buffer: Uint8Array): string {
- let rpos = 0
- let bits = 0
- let vbit = 0
+ let rpos = 0;
+ let bits = 0;
+ let vbit = 0;
- let result = ""
- while ((rpos < buffer.length) || (vbit > 0)) {
- if ((rpos < buffer.length) && (vbit < 5)) {
+ let result = "";
+ while (rpos < buffer.length || vbit > 0) {
+ if (rpos < buffer.length && vbit < 5) {
bits = (bits << 8) | buffer[rpos++];
vbit += 8;
}
if (vbit < 5) {
- bits <<= (5 - vbit);
+ bits <<= 5 - vbit;
vbit = 5;
}
result += encTable__[(bits >> (vbit - 5)) & 31];
vbit -= 5;
}
- return result
+ return result;
}
// const array = new Uint8Array(256)
diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx
index 07bc7c604..d83442e62 100644
--- a/packages/anastasis-webui/src/pages/home/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/index.tsx
@@ -1,25 +1,22 @@
+import { BackupStates, RecoveryStates } from "anastasis-core";
import {
- BackupStates,
- RecoveryStates,
- ReducerStateBackup,
- ReducerStateRecovery
-} from "anastasis-core";
-import {
- ComponentChildren, Fragment,
+ ComponentChildren,
+ Fragment,
FunctionalComponent,
h,
- VNode
+ VNode,
} from "preact";
-import {
- useErrorBoundary
-} from "preact/hooks";
+import { useErrorBoundary } from "preact/hooks";
import { AsyncButton } from "../../components/AsyncButton";
import { Menu } from "../../components/menu";
import { Notifications } from "../../components/Notifications";
-import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis";
+import {
+ AnastasisProvider,
+ useAnastasisContext,
+} from "../../context/anastasis";
import {
AnastasisReducerApi,
- useAnastasisReducer
+ useAnastasisReducer,
} from "../../hooks/use-anastasis-reducer";
import { AttributeEntryScreen } from "./AttributeEntryScreen";
import { AuthenticationEditorScreen } from "./AuthenticationEditorScreen";
@@ -51,7 +48,11 @@ export function withProcessLabel(
}
interface AnastasisClientFrameProps {
- onNext?(): void;
+ onNext?(): Promise<void>;
+ /**
+ * Override for the "back" functionality.
+ */
+ onBack?(): Promise<void>;
title: string;
children: ComponentChildren;
/**
@@ -118,9 +119,27 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
<section class="section is-main-section">
{props.children}
{!props.hideNav ? (
- <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={() => reducer.back()}>Back</button>
- <AsyncButton class="button is-info" data-tooltip={props.hideNext} onClick={next} disabled={props.hideNext !== undefined}>Next</AsyncButton>
+ <div
+ style={{
+ marginTop: "2em",
+ display: "flex",
+ justifyContent: "space-between",
+ }}
+ >
+ <button
+ class="button"
+ onClick={() => (props.onBack ?? reducer.back)()}
+ >
+ Back
+ </button>
+ <AsyncButton
+ class="button is-info"
+ data-tooltip={props.hideNext}
+ onClick={next}
+ disabled={props.hideNext !== undefined}
+ >
+ Next
+ </AsyncButton>
</div>
) : null}
</section>
@@ -141,7 +160,7 @@ const AnastasisClient: FunctionalComponent = () => {
};
function AnastasisClientImpl(): VNode {
- const reducer = useAnastasisContext()
+ const reducer = useAnastasisContext();
if (!reducer) {
return <p>Fatal: Reducer must be in context.</p>;
}
@@ -157,27 +176,19 @@ function AnastasisClientImpl(): VNode {
state.backup_state === BackupStates.CountrySelecting ||
state.recovery_state === RecoveryStates.CountrySelecting
) {
- return (
- <ContinentSelectionScreen />
- );
+ return <ContinentSelectionScreen />;
}
if (
state.backup_state === BackupStates.UserAttributesCollecting ||
state.recovery_state === RecoveryStates.UserAttributesCollecting
) {
- return (
- <AttributeEntryScreen />
- );
+ return <AttributeEntryScreen />;
}
if (state.backup_state === BackupStates.AuthenticationsEditing) {
- return (
- <AuthenticationEditorScreen />
- );
+ return <AuthenticationEditorScreen />;
}
if (state.backup_state === BackupStates.PoliciesReviewing) {
- return (
- <ReviewPoliciesScreen />
- );
+ return <ReviewPoliciesScreen />;
}
if (state.backup_state === BackupStates.SecretEditing) {
return <SecretEditorScreen />;
@@ -196,15 +207,11 @@ function AnastasisClientImpl(): VNode {
}
if (state.recovery_state === RecoveryStates.SecretSelecting) {
- return (
- <SecretSelectionScreen />
- );
+ return <SecretSelectionScreen />;
}
if (state.recovery_state === RecoveryStates.ChallengeSelecting) {
- return (
- <ChallengeOverviewScreen />
- );
+ return <ChallengeOverviewScreen />;
}
if (state.recovery_state === RecoveryStates.ChallengeSolving) {
@@ -212,9 +219,7 @@ function AnastasisClientImpl(): VNode {
}
if (state.recovery_state === RecoveryStates.RecoveryFinished) {
- return (
- <RecoveryFinishedScreen />
- );
+ return <RecoveryFinishedScreen />;
}
if (state.recovery_state === RecoveryStates.ChallengePaying) {
return <ChallengePayingScreen />;
@@ -224,7 +229,9 @@ function AnastasisClientImpl(): VNode {
<AnastasisClientFrame hideNav title="Bug">
<p>Bug: Unknown state.</p>
<div class="buttons is-right">
- <button class="button" onClick={() => reducer.reset()}>Reset</button>
+ <button class="button" onClick={() => reducer.reset()}>
+ Reset
+ </button>
</div>
</AnastasisClientFrame>
);
@@ -236,11 +243,17 @@ function AnastasisClientImpl(): VNode {
function ErrorBanner(): VNode | null {
const reducer = useAnastasisContext();
if (!reducer || !reducer.currentError) return null;
- return (<Notifications removeNotification={reducer.dismissError} notifications={[{
- type: "ERROR",
- message: `Error code: ${reducer.currentError.code}`,
- description: reducer.currentError.hint
- }]} />
+ return (
+ <Notifications
+ removeNotification={reducer.dismissError}
+ notifications={[
+ {
+ type: "ERROR",
+ message: `Error code: ${reducer.currentError.code}`,
+ description: reducer.currentError.hint,
+ },
+ ]}
+ />
);
}
diff --git a/packages/anastasis-webui/src/pages/notfound/index.tsx b/packages/anastasis-webui/src/pages/notfound/index.tsx
index 4e74d1d9f..bb22429b0 100644
--- a/packages/anastasis-webui/src/pages/notfound/index.tsx
+++ b/packages/anastasis-webui/src/pages/notfound/index.tsx
@@ -1,16 +1,16 @@
-import { FunctionalComponent, h } from 'preact';
-import { Link } from 'preact-router/match';
+import { FunctionalComponent, h } from "preact";
+import { Link } from "preact-router/match";
const Notfound: FunctionalComponent = () => {
- return (
- <div>
- <h1>Error 404</h1>
- <p>That page doesn&apos;t exist.</p>
- <Link href="/">
- <h4>Back to Home</h4>
- </Link>
- </div>
- );
+ return (
+ <div>
+ <h1>Error 404</h1>
+ <p>That page doesn&apos;t exist.</p>
+ <Link href="/">
+ <h4>Back to Home</h4>
+ </Link>
+ </div>
+ );
};
export default Notfound;
diff --git a/packages/anastasis-webui/src/pages/profile/index.tsx b/packages/anastasis-webui/src/pages/profile/index.tsx
index 859a83ed4..bcd26370e 100644
--- a/packages/anastasis-webui/src/pages/profile/index.tsx
+++ b/packages/anastasis-webui/src/pages/profile/index.tsx
@@ -1,43 +1,42 @@
-import { FunctionalComponent, h } from 'preact';
-import { useEffect, useState } from 'preact/hooks';
+import { FunctionalComponent, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
interface Props {
- user: string;
+ user: string;
}
const Profile: FunctionalComponent<Props> = (props: Props) => {
- const { user } = props;
- const [time, setTime] = useState<number>(Date.now());
- const [count, setCount] = useState<number>(0);
-
- // gets called when this route is navigated to
- useEffect(() => {
- const timer = window.setInterval(() => setTime(Date.now()), 1000);
-
- // gets called just before navigating away from the route
- return (): void => {
- clearInterval(timer);
- };
- }, []);
-
- // update the current time
- const increment = (): void => {
- setCount(count + 1);
+ const { user } = props;
+ const [time, setTime] = useState<number>(Date.now());
+ const [count, setCount] = useState<number>(0);
+
+ // gets called when this route is navigated to
+ useEffect(() => {
+ const timer = window.setInterval(() => setTime(Date.now()), 1000);
+
+ // gets called just before navigating away from the route
+ return (): void => {
+ clearInterval(timer);
};
+ }, []);
+
+ // update the current time
+ const increment = (): void => {
+ setCount(count + 1);
+ };
- return (
- <div>
- <h1>Profile: {user}</h1>
- <p>This is the user profile for a user named {user}.</p>
+ return (
+ <div>
+ <h1>Profile: {user}</h1>
+ <p>This is the user profile for a user named {user}.</p>
- <div>Current time: {new Date(time).toLocaleString()}</div>
+ <div>Current time: {new Date(time).toLocaleString()}</div>
- <p>
- <button onClick={increment}>Click Me</button> Clicked {count}{' '}
- times.
- </p>
- </div>
- );
+ <p>
+ <button onClick={increment}>Click Me</button> Clicked {count} times.
+ </p>
+ </div>
+ );
};
export default Profile;
diff --git a/packages/anastasis-webui/src/scss/DurationPicker.scss b/packages/anastasis-webui/src/scss/DurationPicker.scss
index a35575324..aa75b9916 100644
--- a/packages/anastasis-webui/src/scss/DurationPicker.scss
+++ b/packages/anastasis-webui/src/scss/DurationPicker.scss
@@ -1,4 +1,3 @@
-
.rdp-picker {
display: flex;
height: 175px;
diff --git a/packages/anastasis-webui/src/scss/_aside.scss b/packages/anastasis-webui/src/scss/_aside.scss
index c9332b252..11809990b 100644
--- a/packages/anastasis-webui/src/scss/_aside.scss
+++ b/packages/anastasis-webui/src/scss/_aside.scss
@@ -19,37 +19,35 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-@include desktop {
- html {
- &.has-aside-left {
- &.has-aside-expanded {
- nav.navbar,
- body {
- padding-left: $aside-width;
- }
- }
- aside.is-placed-left {
- display: block;
+html {
+ &.has-aside-left {
+ &.has-aside-expanded {
+ nav.navbar,
+ body {
+ padding-left: $aside-width;
}
}
+ aside.is-placed-left {
+ display: block;
+ }
}
+}
- aside.aside.is-expanded {
- width: $aside-width;
+aside.aside.is-expanded {
+ width: $aside-width;
- .menu-list {
- @include icon-with-update-mark($aside-icon-width);
+ .menu-list {
+ @include icon-with-update-mark($aside-icon-width);
- span.menu-item-label {
- display: inline-block;
- }
+ span.menu-item-label {
+ display: inline-block;
+ }
- li.is-active {
- ul {
- display: block;
- }
- background-color: $body-background-color;
+ li.is-active {
+ ul {
+ display: block;
}
+ background-color: $body-background-color;
}
}
}
@@ -128,59 +126,3 @@ aside.aside {
margin-bottom: $default-padding * 0.5;
}
}
-
-@include touch {
- nav.navbar {
- @include transition(margin-left);
- }
- aside.aside {
- @include transition(left);
- }
- html.has-aside-mobile-transition {
- body {
- overflow-x: hidden;
- }
- body,
- nav.navbar {
- width: 100vw;
- }
- aside.aside {
- width: $aside-mobile-width;
- display: block;
- left: $aside-mobile-width * -1;
-
- .image {
- img {
- max-width: $aside-mobile-width * 0.33;
- }
- }
-
- .menu-list {
- li.is-active {
- ul {
- display: block;
- }
- background-color: $body-background-color;
- }
- li {
- @include icon-with-update-mark($aside-icon-width);
- margin-top: 8px;
- margin-bottom: 8px;
- }
- a {
- span.menu-item-label {
- display: inline-block;
- }
- }
- }
- }
- }
- div.has-aside-mobile-expanded {
- nav.navbar {
- margin-left: $aside-mobile-width;
- }
- aside.aside {
- left: 0;
- }
- }
-}
diff --git a/packages/anastasis-webui/src/scss/_card.scss b/packages/anastasis-webui/src/scss/_card.scss
index b2eec27a1..3f71aeb6a 100644
--- a/packages/anastasis-webui/src/scss/_card.scss
+++ b/packages/anastasis-webui/src/scss/_card.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -39,7 +39,7 @@
&.is-card-widget {
.card-content {
- padding: $default-padding * .5;
+ padding: $default-padding * 0.5;
}
}
diff --git a/packages/anastasis-webui/src/scss/_custom-calendar.scss b/packages/anastasis-webui/src/scss/_custom-calendar.scss
index bff68cf79..e0334b62d 100644
--- a/packages/anastasis-webui/src/scss/_custom-calendar.scss
+++ b/packages/anastasis-webui/src/scss/_custom-calendar.scss
@@ -16,31 +16,30 @@
:root {
--primary-color: #3298dc;
-
- --primary-text-color-dark: rgba(0,0,0,.87);
- --secondary-text-color-dark: rgba(0,0,0,.57);
- --disabled-text-color-dark: rgba(0,0,0,.13);
-
- --primary-text-color-light: rgba(255,255,255,.87);
- --secondary-text-color-light: rgba(255,255,255,.57);
- --disabled-text-color-light: rgba(255,255,255,.13);
-
- --font-stack: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-
+
+ --primary-text-color-dark: rgba(0, 0, 0, 0.87);
+ --secondary-text-color-dark: rgba(0, 0, 0, 0.57);
+ --disabled-text-color-dark: rgba(0, 0, 0, 0.13);
+
+ --primary-text-color-light: rgba(255, 255, 255, 0.87);
+ --secondary-text-color-light: rgba(255, 255, 255, 0.57);
+ --disabled-text-color-light: rgba(255, 255, 255, 0.13);
+
+ --font-stack: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
+
--primary-card-color: #fff;
--primary-background-color: #f2f2f2;
-
+
--box-shadow-lvl-1: 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 1px 2px rgba(0, 0, 0, 0.24);
+ 0 1px 2px rgba(0, 0, 0, 0.24);
--box-shadow-lvl-2: 0 3px 6px rgba(0, 0, 0, 0.16),
- 0 3px 6px rgba(0, 0, 0, 0.23);
+ 0 3px 6px rgba(0, 0, 0, 0.23);
--box-shadow-lvl-3: 0 10px 20px rgba(0, 0, 0, 0.19),
- 0 6px 6px rgba(0, 0, 0, 0.23);
+ 0 6px 6px rgba(0, 0, 0, 0.23);
--box-shadow-lvl-4: 0 14px 28px rgba(0, 0, 0, 0.25),
- 0 10px 10px rgba(0, 0, 0, 0.22);
+ 0 10px 10px rgba(0, 0, 0, 0.22);
}
-
.home .datePicker div {
margin-top: 0px;
margin-bottom: 0px;
@@ -56,7 +55,7 @@
width: 90vw;
max-width: 448px;
transform-origin: top left;
- transition: transform .22s ease-in-out, opacity .22s ease-in-out;
+ transition: transform 0.22s ease-in-out, opacity 0.22s ease-in-out;
top: 50%;
left: 50%;
opacity: 0;
@@ -67,7 +66,7 @@
opacity: 1;
transform: scale(1) translate(-50%, -50%);
}
-
+
.datePicker--titles {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
@@ -75,7 +74,8 @@
height: 100px;
background: var(--primary-color);
- h2, h3 {
+ h2,
+ h3 {
cursor: pointer;
color: #fff;
line-height: 1;
@@ -85,7 +85,7 @@
}
h3 {
- color: rgba(255,255,255,.57);
+ color: rgba(255, 255, 255, 0.57);
font-size: 18px;
padding-bottom: 2px;
}
@@ -114,13 +114,13 @@
font-size: 26px;
user-select: none;
border-radius: 50%;
-
+
&:hover {
background: var(--disabled-text-color-dark);
}
}
}
-
+
.datePicker--scroll {
overflow-y: auto;
max-height: calc(90vh - 56px - 100px);
@@ -133,9 +133,11 @@
width: 100%;
display: grid;
text-align: center;
-
+
// there's probably a better way to do this, but wanted to try out CSS grid
- grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7);
+ grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(
+ 100% / 7
+ ) calc(100% / 7) calc(100% / 7) calc(100% / 7);
span {
color: var(--secondary-text-color-dark);
@@ -149,14 +151,16 @@
width: 100%;
display: grid;
text-align: center;
- grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7);
+ grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(
+ 100% / 7
+ ) calc(100% / 7) calc(100% / 7) calc(100% / 7);
span {
color: var(--primary-text-color-dark);
line-height: 42px;
font-size: 14px;
display: inline-grid;
- transition: color .22s;
+ transition: color 0.22s;
height: 42px;
position: relative;
cursor: pointer;
@@ -164,7 +168,7 @@
border-radius: 50%;
&::before {
- content: '';
+ content: "";
position: absolute;
z-index: -1;
height: 42px;
@@ -172,12 +176,12 @@
left: calc(50% - 21px);
background: var(--primary-color);
border-radius: 50%;
- transition: transform .22s, opacity .22s;
+ transition: transform 0.22s, opacity 0.22s;
transform: scale(0);
opacity: 0;
}
-
- &[disabled=true] {
+
+ &[disabled="true"] {
cursor: unset;
}
@@ -186,7 +190,7 @@
}
&.datePicker--selected {
- color: rgba(255,255,255,.87);
+ color: rgba(255, 255, 255, 0.87);
&:before {
transform: scale(1);
@@ -196,21 +200,21 @@
}
}
}
-
+
.datePicker--selectYear {
padding: 0 20px;
display: block;
width: 100%;
text-align: center;
max-height: 362px;
-
+
span {
display: block;
width: 100%;
font-size: 24px;
margin: 20px auto;
cursor: pointer;
-
+
&.selected {
font-size: 42px;
color: var(--primary-color);
@@ -236,9 +240,10 @@
appearance: none;
padding: 0 16px;
border-radius: 3px;
- transition: background-color .13s;
+ transition: background-color 0.13s;
- &:hover, &:focus {
+ &:hover,
+ &:focus {
outline: none;
background-color: var(--disabled-text-color-dark);
}
@@ -253,6 +258,6 @@
left: 0;
bottom: 0;
right: 0;
- background: rgba(0,0,0,.52);
- animation: fadeIn .22s forwards;
+ background: rgba(0, 0, 0, 0.52);
+ animation: fadeIn 0.22s forwards;
}
diff --git a/packages/anastasis-webui/src/scss/_footer.scss b/packages/anastasis-webui/src/scss/_footer.scss
index 027a5ca8b..112522ed8 100644
--- a/packages/anastasis-webui/src/scss/_footer.scss
+++ b/packages/anastasis-webui/src/scss/_footer.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/anastasis-webui/src/scss/_form.scss b/packages/anastasis-webui/src/scss/_form.scss
index 71f0d4da4..786044eff 100644
--- a/packages/anastasis-webui/src/scss/_form.scss
+++ b/packages/anastasis-webui/src/scss/_form.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -22,11 +22,12 @@
.field {
&.has-check {
.field-body {
- margin-top: $default-padding * .125;
+ margin-top: $default-padding * 0.125;
}
}
.control {
- .mdi-24px.mdi-set, .mdi-24px.mdi:before {
+ .mdi-24px.mdi-set,
+ .mdi-24px.mdi:before {
font-size: inherit;
}
}
@@ -37,28 +38,34 @@
}
}
-.input, .textarea, select {
+.input,
+.textarea,
+select {
box-shadow: none;
- &:focus, &:active {
- box-shadow: none!important;
+ &:focus,
+ &:active {
+ box-shadow: none !important;
}
}
-.switch input[type=checkbox]+.check:before {
+.switch input[type="checkbox"] + .check:before {
box-shadow: none;
}
-.switch, .b-checkbox.checkbox {
- input[type=checkbox] {
- &:focus + .check, &:focus:checked + .check {
- box-shadow: none!important;
+.switch,
+.b-checkbox.checkbox {
+ input[type="checkbox"] {
+ &:focus + .check,
+ &:focus:checked + .check {
+ box-shadow: none !important;
}
}
}
-.b-checkbox.checkbox input[type=checkbox], .b-radio.radio input[type=radio] {
- &+.check {
+.b-checkbox.checkbox input[type="checkbox"],
+.b-radio.radio input[type="radio"] {
+ & + .check {
border: $checkbox-border;
}
}
diff --git a/packages/anastasis-webui/src/scss/_hero-bar.scss b/packages/anastasis-webui/src/scss/_hero-bar.scss
index 90b67a2ed..31b7e623e 100644
--- a/packages/anastasis-webui/src/scss/_hero-bar.scss
+++ b/packages/anastasis-webui/src/scss/_hero-bar.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -32,17 +32,17 @@ section.hero.is-hero-bar {
}
> div > .level {
- margin-bottom: $default-padding * .5;
+ margin-bottom: $default-padding * 0.5;
}
.subtitle + p {
- margin-top: $default-padding * .5;
+ margin-top: $default-padding * 0.5;
}
}
.button {
&.is-hero-button {
- background-color: rgba($white, .5);
+ background-color: rgba($white, 0.5);
font-weight: 300;
@include transition(background-color);
diff --git a/packages/anastasis-webui/src/scss/_main-section.scss b/packages/anastasis-webui/src/scss/_main-section.scss
index 1a4fad81d..01edc24bf 100644
--- a/packages/anastasis-webui/src/scss/_main-section.scss
+++ b/packages/anastasis-webui/src/scss/_main-section.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/anastasis-webui/src/scss/_mixins.scss b/packages/anastasis-webui/src/scss/_mixins.scss
index 0809033ed..b52e590e3 100644
--- a/packages/anastasis-webui/src/scss/_mixins.scss
+++ b/packages/anastasis-webui/src/scss/_mixins.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -23,12 +23,12 @@
transition: $t 250ms ease-in-out 50ms;
}
-@mixin icon-with-update-mark ($icon-base-width) {
+@mixin icon-with-update-mark($icon-base-width) {
.icon {
width: $icon-base-width;
&.has-update-mark:after {
- right: ($icon-base-width / 2) - .85;
+ right: ($icon-base-width / 2) - 0.85;
}
}
}
diff --git a/packages/anastasis-webui/src/scss/_modal.scss b/packages/anastasis-webui/src/scss/_modal.scss
index 3edbb8d3a..b3a31ebf1 100644
--- a/packages/anastasis-webui/src/scss/_modal.scss
+++ b/packages/anastasis-webui/src/scss/_modal.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
diff --git a/packages/anastasis-webui/src/scss/_nav-bar.scss b/packages/anastasis-webui/src/scss/_nav-bar.scss
index 09f1e2326..c6dd04263 100644
--- a/packages/anastasis-webui/src/scss/_nav-bar.scss
+++ b/packages/anastasis-webui/src/scss/_nav-bar.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -25,7 +25,7 @@ nav.navbar {
.navbar-item {
&.has-user-avatar {
.is-user-avatar {
- margin-right: $default-padding * .5;
+ margin-right: $default-padding * 0.5;
display: inline-flex;
width: $navbar-avatar-size;
height: $navbar-avatar-size;
@@ -98,11 +98,11 @@ nav.navbar {
.navbar-item {
.icon:first-child {
- margin-right: $default-padding * .5;
+ margin-right: $default-padding * 0.5;
}
&.has-dropdown {
- >.navbar-link {
+ > .navbar-link {
background-color: $white-ter;
.icon:last-child {
display: none;
@@ -111,11 +111,11 @@ nav.navbar {
}
&.has-user-avatar {
- >.navbar-link {
+ > .navbar-link {
display: flex;
align-items: center;
- padding-top: $default-padding * .5;
- padding-bottom: $default-padding * .5;
+ padding-top: $default-padding * 0.5;
+ padding-bottom: $default-padding * 0.5;
}
}
}
@@ -131,7 +131,7 @@ nav.navbar {
&:not(.is-desktop-icon-only) {
.icon:first-child {
- margin-right: $default-padding * .5;
+ margin-right: $default-padding * 0.5;
}
}
&.is-desktop-icon-only {
diff --git a/packages/anastasis-webui/src/scss/_table.scss b/packages/anastasis-webui/src/scss/_table.scss
index 9cf6f4dcd..b68d50e4f 100644
--- a/packages/anastasis-webui/src/scss/_table.scss
+++ b/packages/anastasis-webui/src/scss/_table.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -26,7 +26,8 @@ table.table {
}
}
- td, th {
+ td,
+ th {
&.checkbox-cell {
.b-checkbox.checkbox:not(.button) {
margin-right: 0;
@@ -83,7 +84,9 @@ table.table {
}
}
- .pagination-previous, .pagination-next, .pagination-link {
+ .pagination-previous,
+ .pagination-next,
+ .pagination-link {
border-color: $button-border-color;
color: $base-color;
@@ -108,24 +111,25 @@ table.table {
&.has-mobile-sort-spaced {
.b-table {
.field.table-mobile-sort {
- padding-top: $default-padding * .5;
+ padding-top: $default-padding * 0.5;
}
}
}
}
.b-table {
.field.table-mobile-sort {
- padding: 0 $default-padding * .5;
+ padding: 0 $default-padding * 0.5;
}
.table-wrapper.has-mobile-cards {
tr {
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
- margin-bottom: 3px!important;
+ margin-bottom: 3px !important;
}
td {
&.is-progress-col {
- span, progress {
+ span,
+ progress {
display: flex;
width: 45%;
align-items: center;
@@ -133,11 +137,13 @@ table.table {
}
}
- &.checkbox-cell, &.is-image-cell {
- border-bottom: 0!important;
+ &.checkbox-cell,
+ &.is-image-cell {
+ border-bottom: 0 !important;
}
- &.checkbox-cell, &.is-actions-cell {
+ &.checkbox-cell,
+ &.is-actions-cell {
&:before {
display: none;
}
@@ -163,7 +169,7 @@ table.table {
.image {
width: $table-avatar-size-mobile;
height: auto;
- margin: 0 auto $default-padding * .25;
+ margin: 0 auto $default-padding * 0.25;
}
}
}
diff --git a/packages/anastasis-webui/src/scss/_tiles.scss b/packages/anastasis-webui/src/scss/_tiles.scss
index 94fc04e70..e69d995f0 100644
--- a/packages/anastasis-webui/src/scss/_tiles.scss
+++ b/packages/anastasis-webui/src/scss/_tiles.scss
@@ -14,12 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-
.is-tiles-wrapper {
margin-bottom: $default-padding;
}
diff --git a/packages/anastasis-webui/src/scss/_title-bar.scss b/packages/anastasis-webui/src/scss/_title-bar.scss
index 736f26cbd..932f8e65d 100644
--- a/packages/anastasis-webui/src/scss/_title-bar.scss
+++ b/packages/anastasis-webui/src/scss/_title-bar.scss
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
+/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
@@ -26,14 +26,14 @@ section.section.is-title-bar {
ul {
li {
display: inline-block;
- padding: 0 $default-padding * .5 0 0;
+ padding: 0 $default-padding * 0.5 0 0;
font-size: $default-padding;
color: $title-bar-color;
&:after {
display: inline-block;
- content: '/';
- padding-left: $default-padding * .5;
+ content: "/";
+ padding-left: $default-padding * 0.5;
}
&:last-child {
diff --git a/packages/anastasis-webui/src/scss/main.scss b/packages/anastasis-webui/src/scss/main.scss
index b5335073f..9311fbba0 100644
--- a/packages/anastasis-webui/src/scss/main.scss
+++ b/packages/anastasis-webui/src/scss/main.scss
@@ -190,7 +190,6 @@ div[data-tooltip]::before {
border: solid 1px #f2e9bf;
}
-
.home {
padding: 1em 1em;
min-height: 100%;
@@ -218,9 +217,9 @@ div[data-tooltip]::before {
}
.profile {
- padding: 56px 20px;
- min-height: 100%;
- width: 100%;
+ padding: 56px 20px;
+ min-height: 100%;
+ width: 100%;
}
.notfound {
@@ -232,4 +231,4 @@ h1 {
font-size: 1.5em;
margin-top: 0.8em;
margin-bottom: 0.8em;
-} \ No newline at end of file
+}
diff --git a/packages/anastasis-webui/src/template.html b/packages/anastasis-webui/src/template.html
index 351f1829c..8ae2fe104 100644
--- a/packages/anastasis-webui/src/template.html
+++ b/packages/anastasis-webui/src/template.html
@@ -1,15 +1,51 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+ @author Sebastian Javier Marchano
+-->
<!DOCTYPE html>
-<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
- <head>
- <meta charset="utf-8">
- <title><% preact.title %></title>
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <meta name="mobile-web-app-capable" content="yes">
- <meta name="apple-mobile-web-app-capable" content="yes">
- <link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png">
- <% preact.headEnd %>
- </head>
- <body>
- <% preact.bodyEnd %>
- </body>
+<html
+ lang="en"
+ class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
+>
+ <head>
+ <meta charset="utf-8" />
+ <title><%= htmlWebpackPlugin.options.title %></title>
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+
+ <% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
+ <meta
+ name="theme-color"
+ content="<%= htmlWebpackPlugin.options.manifest.theme_color %>"
+ />
+ <% } %>
+ </head>
+ <body>
+ <script>
+ <%= compilation.assets[htmlWebpackPlugin.files.chunks["polyfills"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
+ </script>
+ <script>
+ <%= compilation.assets[htmlWebpackPlugin.files.chunks["bundle"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
+ </script>
+ </body>
</html>
diff --git a/packages/anastasis-webui/src/utils/index.tsx b/packages/anastasis-webui/src/utils/index.tsx
index 9c01aa6ba..a8f6c3101 100644
--- a/packages/anastasis-webui/src/utils/index.tsx
+++ b/packages/anastasis-webui/src/utils/index.tsx
@@ -1,45 +1,67 @@
/* eslint-disable @typescript-eslint/camelcase */
-import { BackupStates, RecoveryStates, ReducerState } from 'anastasis-core';
-import { FunctionalComponent, h, VNode } from 'preact';
-import { AnastasisProvider } from '../context/anastasis';
+import { BackupStates, RecoveryStates, ReducerState } from "anastasis-core";
+import { FunctionalComponent, h, VNode } from "preact";
+import { AnastasisProvider } from "../context/anastasis";
-export function createExample<Props>(Component: FunctionalComponent<Props>, currentReducerState?: ReducerState, props?: Partial<Props>): { (args: Props): VNode } {
+export function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ currentReducerState?: ReducerState,
+ props?: Partial<Props>,
+): { (args: Props): VNode } {
const r = (args: Props): VNode => {
- return <AnastasisProvider value={{
- currentReducerState,
- currentError: undefined,
- back: async () => { null },
- dismissError: async () => { null },
- reset: () => { null },
- runTransaction: async () => { null },
- startBackup: () => { null },
- startRecover: () => { null },
- transition: async () => { null },
- }}>
- <Component {...args} />
- </AnastasisProvider>
- }
- r.args = props
- return r
+ return (
+ <AnastasisProvider
+ value={{
+ currentReducerState,
+ currentError: undefined,
+ back: async () => {
+ null;
+ },
+ dismissError: async () => {
+ null;
+ },
+ reset: () => {
+ null;
+ },
+ runTransaction: async () => {
+ null;
+ },
+ startBackup: () => {
+ null;
+ },
+ startRecover: () => {
+ null;
+ },
+ transition: async () => {
+ null;
+ },
+ }}
+ >
+ <Component {...args} />
+ </AnastasisProvider>
+ );
+ };
+ r.args = props;
+ return r;
}
const base = {
continents: [
{
- name: "Europe"
+ name: "Europe",
},
{
- name: "India"
+ name: "India",
},
{
- name: "Asia"
+ name: "Asia",
},
{
- name: "North America"
+ name: "North America",
},
{
- name: "Testcontinent"
- }
+ name: "Testcontinent",
+ },
],
countries: [
{
@@ -47,122 +69,124 @@ const base = {
name: "Testland",
continent: "Testcontinent",
continent_i18n: {
- de_DE: "Testkontinent"
+ de_DE: "Testkontinent",
},
name_i18n: {
de_DE: "Testlandt",
de_CH: "Testlandi",
fr_FR: "Testpais",
- en_UK: "Testland"
+ en_UK: "Testland",
},
currency: "TESTKUDOS",
- call_code: "+00"
+ call_code: "+00",
},
{
code: "xy",
name: "Demoland",
continent: "Testcontinent",
continent_i18n: {
- de_DE: "Testkontinent"
+ de_DE: "Testkontinent",
},
name_i18n: {
de_DE: "Demolandt",
de_CH: "Demolandi",
fr_FR: "Demopais",
- en_UK: "Demoland"
+ en_UK: "Demoland",
},
currency: "KUDOS",
- call_code: "+01"
- }
+ call_code: "+01",
+ },
],
authentication_providers: {
"http://localhost:8086/": {
http_status: 200,
annual_fee: "COL:0",
- business_name: "ana",
+ business_name: "Anastasis Local",
currency: "COL",
liability_limit: "COL:10",
methods: [
{
type: "question",
- usage_fee: "COL:0"
- }, {
+ usage_fee: "COL:0",
+ },
+ {
type: "sms",
- usage_fee: "COL:0"
- }, {
+ usage_fee: "COL:0",
+ },
+ {
type: "email",
- usage_fee: "COL:0"
+ usage_fee: "COL:0",
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
- truth_upload_fee: "COL:0"
+ truth_upload_fee: "COL:0",
},
"https://kudos.demo.anastasis.lu/": {
http_status: 200,
annual_fee: "COL:0",
- business_name: "ana",
+ business_name: "Anastasis Kudo",
currency: "COL",
liability_limit: "COL:10",
methods: [
{
type: "question",
- usage_fee: "COL:0"
- }, {
+ usage_fee: "COL:0",
+ },
+ {
type: "email",
- usage_fee: "COL:0"
+ usage_fee: "COL:0",
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
- truth_upload_fee: "COL:0"
+ truth_upload_fee: "COL:0",
},
"https://anastasis.demo.taler.net/": {
http_status: 200,
annual_fee: "COL:0",
- business_name: "ana",
+ business_name: "Anastasis Demo",
currency: "COL",
liability_limit: "COL:10",
methods: [
{
type: "question",
- usage_fee: "COL:0"
- }, {
+ usage_fee: "COL:0",
+ },
+ {
type: "sms",
- usage_fee: "COL:0"
- }, {
+ usage_fee: "COL:0",
+ },
+ {
type: "totp",
- usage_fee: "COL:0"
+ usage_fee: "COL:0",
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
- truth_upload_fee: "COL:0"
+ truth_upload_fee: "COL:0",
},
"http://localhost:8087/": {
code: 8414,
- hint: "request to provider failed"
+ hint: "request to provider failed",
},
"http://localhost:8088/": {
code: 8414,
- hint: "request to provider failed"
+ hint: "request to provider failed",
},
"http://localhost:8089/": {
code: 8414,
- hint: "request to provider failed"
- }
+ hint: "request to provider failed",
+ },
},
- // expiration: {
- // d_ms: 1792525051855 // check t_ms
- // },
-} as Partial<ReducerState>
+} as Partial<ReducerState>;
export const reducerStatesExample = {
initial: undefined,
recoverySelectCountry: {
...base,
- recovery_state: RecoveryStates.CountrySelecting
+ recovery_state: RecoveryStates.CountrySelecting,
} as ReducerState,
recoverySelectContinent: {
...base,
@@ -190,11 +214,11 @@ export const reducerStatesExample = {
} as ReducerState,
recoveryAttributeEditing: {
...base,
- recovery_state: RecoveryStates.UserAttributesCollecting
+ recovery_state: RecoveryStates.UserAttributesCollecting,
} as ReducerState,
backupSelectCountry: {
...base,
- backup_state: BackupStates.CountrySelecting
+ backup_state: BackupStates.CountrySelecting,
} as ReducerState,
backupSelectContinent: {
...base,
@@ -218,15 +242,14 @@ export const reducerStatesExample = {
} as ReducerState,
authEditing: {
...base,
- backup_state: BackupStates.AuthenticationsEditing
+ backup_state: BackupStates.AuthenticationsEditing,
} as ReducerState,
backupAttributeEditing: {
...base,
- backup_state: BackupStates.UserAttributesCollecting
+ backup_state: BackupStates.UserAttributesCollecting,
} as ReducerState,
truthsPaying: {
...base,
- backup_state: BackupStates.TruthsPaying
+ backup_state: BackupStates.TruthsPaying,
} as ReducerState,
-
-}
+};
diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts
index 70e52e63b..ecdd6fdf8 100644
--- a/packages/taler-util/src/backupTypes.ts
+++ b/packages/taler-util/src/backupTypes.ts
@@ -53,6 +53,7 @@
/**
* Imports.
*/
+import { DenominationPubKey, UnblindedSignature } from "./talerTypes.js";
import { Duration, Timestamp } from "./time.js";
/**
@@ -440,7 +441,7 @@ export interface BackupCoin {
/**
* Unblinded signature by the exchange.
*/
- denom_sig: string;
+ denom_sig: UnblindedSignature;
/**
* Amount that's left on the coin.
@@ -831,7 +832,7 @@ export interface BackupDenomination {
/**
* The denomination public key.
*/
- denom_pub: string;
+ denom_pub: DenominationPubKey;
/**
* Fee for withdrawing.
diff --git a/packages/taler-util/src/helpers.ts b/packages/taler-util/src/helpers.ts
index 089602c9d..6c836c482 100644
--- a/packages/taler-util/src/helpers.ts
+++ b/packages/taler-util/src/helpers.ts
@@ -94,7 +94,7 @@ export function canonicalJson(obj: any): string {
/**
* Lexically compare two strings.
*/
-export function strcmp(s1: string, s2: string): number {
+export function strcmp(s1: string, s2: string): -1 | 0 | 1 {
if (s1 < s2) {
return -1;
}
@@ -113,15 +113,14 @@ export function j2s(x: any): string {
/**
* Use this to filter null or undefined from an array in a type-safe fashion
- *
+ *
* example:
* const array: Array<T | undefined> = [undefined, null]
* const filtered: Array<T> = array.filter(notEmpty)
- *
- * @param value
- * @returns
+ *
+ * @param value
+ * @returns
*/
export function notEmpty<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
-
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index 504db533b..fc3380555 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -16,12 +16,31 @@
import { URLSearchParams } from "./url.js";
-interface PaytoUri {
+export type PaytoUri = PaytoUriUnknown | PaytoUriIBAN | PaytoUriTalerBank;
+
+interface PaytoUriGeneric {
targetType: string;
targetPath: string;
params: { [name: string]: string };
}
+interface PaytoUriUnknown extends PaytoUriGeneric {
+ isKnown: false;
+}
+
+interface PaytoUriIBAN extends PaytoUriGeneric {
+ isKnown: true;
+ targetType: 'iban',
+ iban: string;
+}
+
+interface PaytoUriTalerBank extends PaytoUriGeneric {
+ isKnown: true;
+ targetType: 'x-taler-bank',
+ host: string;
+ account: string;
+}
+
const paytoPfx = "payto://";
/**
@@ -63,9 +82,33 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
params[v] = k;
});
+ if (targetType === 'x-taler-bank') {
+ const parts = targetPath.split('/')
+ const host = parts[0]
+ const account = parts[1]
+ return {
+ targetPath,
+ targetType,
+ params,
+ isKnown: true,
+ host, account,
+ };
+
+ }
+ if (targetType === 'iban') {
+ return {
+ isKnown: true,
+ targetPath,
+ targetType,
+ params,
+ iban: targetPath
+ };
+
+ }
return {
targetPath,
targetType,
params,
+ isKnown: false
};
}
diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts
index 536c4dc48..b107786cd 100644
--- a/packages/taler-util/src/talerCrypto.ts
+++ b/packages/taler-util/src/talerCrypto.ts
@@ -24,6 +24,7 @@
import * as nacl from "./nacl-fast.js";
import { kdf } from "./kdf.js";
import bigint from "big-integer";
+import { DenominationPubKey } from "./talerTypes.js";
export function getRandomBytes(n: number): Uint8Array {
return nacl.randomBytes(n);
@@ -161,10 +162,6 @@ interface RsaPub {
e: bigint.BigInteger;
}
-interface RsaBlindingKey {
- r: bigint.BigInteger;
-}
-
/**
* KDF modulo a big integer.
*/
@@ -352,6 +349,20 @@ export function hash(d: Uint8Array): Uint8Array {
return nacl.hash(d);
}
+export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
+ if (pub.cipher !== 1) {
+ throw Error("unsupported cipher");
+ }
+ const pubBuf = decodeCrock(pub.rsa_public_key);
+ const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
+ const uint8ArrayBuf = new Uint8Array(hashInputBuf);
+ const dv = new DataView(hashInputBuf);
+ dv.setUint32(0, pub.age_mask ?? 0);
+ dv.setUint32(4, pub.cipher);
+ uint8ArrayBuf.set(pubBuf, 8);
+ return nacl.hash(uint8ArrayBuf);
+}
+
export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
return nacl.sign_detached(msg, pair.secretKey);
diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts
index 56110ec1e..04d700483 100644
--- a/packages/taler-util/src/talerTypes.ts
+++ b/packages/taler-util/src/talerTypes.ts
@@ -59,7 +59,7 @@ export class Denomination {
/**
* Public signing key of the denomination.
*/
- denom_pub: string;
+ denom_pub: DenominationPubKey;
/**
* Fee for withdrawing.
@@ -158,7 +158,7 @@ export interface RecoupRequest {
/**
* Signature over the coin public key by the denomination.
*/
- denom_sig: string;
+ denom_sig: UnblindedSignature;
/**
* Coin public key of the coin we want to refund.
@@ -198,6 +198,11 @@ export interface RecoupConfirmation {
old_coin_pub?: string;
}
+export interface UnblindedSignature {
+ cipher: DenomKeyType.Rsa;
+ rsa_signature: string;
+}
+
/**
* Deposit permission for a single coin.
*/
@@ -213,7 +218,7 @@ export interface CoinDepositPermission {
/**
* Signature made by the denomination public key.
*/
- ub_sig: string;
+ ub_sig: UnblindedSignature;
/**
* The denomination public key associated with this coin.
*/
@@ -779,8 +784,38 @@ export class TipPickupGetResponse {
expiration: Timestamp;
}
+export enum DenomKeyType {
+ Rsa = 1,
+ ClauseSchnorr = 2,
+}
+
+export interface RsaBlindedDenominationSignature {
+ cipher: DenomKeyType.Rsa;
+ blinded_rsa_signature: string;
+}
+
+export interface CSBlindedDenominationSignature {
+ cipher: DenomKeyType.ClauseSchnorr;
+}
+
+export type BlindedDenominationSignature =
+ | RsaBlindedDenominationSignature
+ | CSBlindedDenominationSignature;
+
+export const codecForBlindedDenominationSignature = () =>
+ buildCodecForUnion<BlindedDenominationSignature>()
+ .discriminateOn("cipher")
+ .alternative(1, codecForRsaBlindedDenominationSignature())
+ .build("BlindedDenominationSignature");
+
+export const codecForRsaBlindedDenominationSignature = () =>
+ buildCodecForObject<RsaBlindedDenominationSignature>()
+ .property("cipher", codecForConstNumber(1))
+ .property("blinded_rsa_signature", codecForString())
+ .build("RsaBlindedDenominationSignature");
+
export class WithdrawResponse {
- ev_sig: string;
+ ev_sig: BlindedDenominationSignature;
}
/**
@@ -792,7 +827,7 @@ export interface CoinDumpJson {
/**
* The coin's denomination's public key.
*/
- denom_pub: string;
+ denom_pub: DenominationPubKey;
/**
* Hash of denom_pub.
*/
@@ -875,7 +910,7 @@ export interface ExchangeMeltResponse {
}
export interface ExchangeRevealItem {
- ev_sig: string;
+ ev_sig: BlindedDenominationSignature;
}
export interface ExchangeRevealResponse {
@@ -994,6 +1029,30 @@ export interface BankWithdrawalOperationPostResponse {
transfer_done: boolean;
}
+export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
+
+export interface RsaDenominationPubKey {
+ cipher: 1;
+ rsa_public_key: string;
+ age_mask?: number;
+}
+
+export interface CsDenominationPubKey {
+ cipher: 2;
+}
+
+export const codecForDenominationPubKey = () =>
+ buildCodecForUnion<DenominationPubKey>()
+ .discriminateOn("cipher")
+ .alternative(1, codecForRsaDenominationPubKey())
+ .build("DenominationPubKey");
+
+export const codecForRsaDenominationPubKey = () =>
+ buildCodecForObject<RsaDenominationPubKey>()
+ .property("cipher", codecForConstNumber(1))
+ .property("rsa_public_key", codecForString())
+ .build("DenominationPubKey");
+
export const codecForBankWithdrawalOperationPostResponse = (): Codec<BankWithdrawalOperationPostResponse> =>
buildCodecForObject<BankWithdrawalOperationPostResponse>()
.property("transfer_done", codecForBoolean())
@@ -1008,7 +1067,7 @@ export type CoinPublicKeyString = string;
export const codecForDenomination = (): Codec<Denomination> =>
buildCodecForObject<Denomination>()
.property("value", codecForString())
- .property("denom_pub", codecForString())
+ .property("denom_pub", codecForDenominationPubKey())
.property("fee_withdraw", codecForString())
.property("fee_deposit", codecForString())
.property("fee_refresh", codecForString())
@@ -1242,7 +1301,7 @@ export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
buildCodecForObject<WithdrawResponse>()
- .property("ev_sig", codecForString())
+ .property("ev_sig", codecForBlindedDenominationSignature())
.build("WithdrawResponse");
export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
@@ -1260,7 +1319,7 @@ export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
buildCodecForObject<ExchangeRevealItem>()
- .property("ev_sig", codecForString())
+ .property("ev_sig", codecForBlindedDenominationSignature())
.build("ExchangeRevealItem");
export const codecForExchangeRevealResponse = (): Codec<ExchangeRevealResponse> =>
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
index 09c70682a..b487c73ae 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -57,6 +57,13 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
const host = parts[0].toLowerCase();
const pathSegments = parts.slice(1, parts.length - 1);
+ /**
+ * The statement below does not tolerate a slash-ended URI.
+ * This results in (1) the withdrawalId being passed as the
+ * empty string, and (2) the bankIntegrationApi ending with the
+ * actual withdrawal operation ID. That can be fixed by
+ * trimming the parts-list. FIXME
+ */
const withdrawId = parts[parts.length - 1];
const p = [host, ...pathSegments].join("/");
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
index 6e68ee080..879640e82 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -48,6 +48,8 @@ import {
AmountString,
codecForContractTerms,
ContractTerms,
+ DenominationPubKey,
+ UnblindedSignature,
} from "./talerTypes.js";
import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
import { BackupRecovery } from "./backupTypes.js";
@@ -454,7 +456,7 @@ export interface PlanchetCreationResult {
coinPriv: string;
reservePub: string;
denomPubHash: string;
- denomPub: string;
+ denomPub: DenominationPubKey;
blindingKey: string;
withdrawSig: string;
coinEv: string;
@@ -467,7 +469,7 @@ export interface PlanchetCreationRequest {
coinIndex: number;
value: AmountJson;
feeWithdraw: AmountJson;
- denomPub: string;
+ denomPub: DenominationPubKey;
reservePub: string;
reservePriv: string;
}
@@ -514,7 +516,7 @@ export interface DepositInfo {
feeDeposit: AmountJson;
wireInfoHash: string;
denomPubHash: string;
- denomSig: string;
+ denomSig: UnblindedSignature;
}
export interface ExchangesListRespose {
diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-wallet-cli/src/harness/harness.ts
index f8dd15738..4944e3471 100644
--- a/packages/taler-wallet-cli/src/harness/harness.ts
+++ b/packages/taler-wallet-cli/src/harness/harness.ts
@@ -65,6 +65,8 @@ import {
EddsaKeyPair,
encodeCrock,
getRandomBytes,
+ hash,
+ stringToBytes
} from "@gnu-taler/taler-util";
import { CoinConfig } from "./denomStructures.js";
import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js";
@@ -431,8 +433,7 @@ function setCoin(config: Configuration, c: CoinConfig) {
}
/**
- * Send an HTTP request until it succeeds or the
- * process dies.
+ * Send an HTTP request until it suceeds or the process dies.
*/
export async function pingProc(
proc: ProcessWrapper | undefined,
@@ -523,22 +524,26 @@ export namespace BankApi {
password: string,
): Promise<BankUser> {
const url = new URL("testing/register", bank.baseUrl);
- await axios.post(url.href, {
+ let resp = await axios.post(url.href, {
username,
password,
});
+ let paytoUri = `payto://x-taler-bank/localhost/${username}`;
+ if (process.env.WALLET_HARNESS_WITH_EUFIN) {
+ paytoUri = resp.data.paytoUri;
+ }
return {
password,
username,
- accountPaytoUri: `payto://x-taler-bank/localhost/${username}`,
+ accountPaytoUri: paytoUri,
};
}
export async function createRandomBankUser(
bank: BankServiceInterface,
): Promise<BankUser> {
- const username = "user-" + encodeCrock(getRandomBytes(10));
- const password = "pw-" + encodeCrock(getRandomBytes(10));
+ const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase();
+ const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase();
return await registerAccount(bank, username, password);
}
@@ -551,9 +556,14 @@ export namespace BankApi {
debitAccountPayto: string;
},
) {
- const url = new URL(
+
+ let maybeBaseUrl = bank.baseUrl;
+ if (process.env.WALLET_HARNESS_WITH_EUFIN) {
+ maybeBaseUrl = (bank as EufinBankService).baseUrlDemobank;
+ }
+ let url = new URL(
`taler-wire-gateway/${params.exchangeBankAccount.accountName}/admin/add-incoming`,
- bank.baseUrl,
+ maybeBaseUrl,
);
await axios.post(
url.href,
@@ -623,28 +633,55 @@ class BankServiceBase {
* Work in progress. The key point is that both Sandbox and Nexus
* will be configured and started by this class.
*/
-class LibeufinBankService extends BankServiceBase implements BankService {
+class EufinBankService extends BankServiceBase implements BankServiceInterface {
sandboxProc: ProcessWrapper | undefined;
nexusProc: ProcessWrapper | undefined;
static async create(
gc: GlobalTestState,
bc: BankConfig,
- ): Promise<BankService> {
+ ): Promise<EufinBankService> {
- return new LibeufinBankService(gc, bc, "foo");
+ return new EufinBankService(gc, bc, "foo");
}
get port() {
return this.bankConfig.httpPort;
}
+ get nexusPort() {
+ return this.bankConfig.httpPort + 1000;
+
+ }
+
+ get nexusDbConn(): string {
+ return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`;
+ }
+
+ get sandboxDbConn(): string {
+ return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`;
+
+ }
get nexusBaseUrl(): string {
- return `http://localhost:${this.bankConfig.httpPort + 1}`;
+ return `http://localhost:${this.nexusPort}`;
+ }
+
+ get baseUrlDemobank(): string {
+ let url = new URL("demobanks/default/", this.baseUrlNetloc);
+ return url.href;
+ }
+
+ get baseUrlAccessApi(): string {
+ let url = new URL("access-api/", this.baseUrlDemobank);
+ return url.href;
+ }
+
+ get baseUrlNetloc(): string {
+ return `http://localhost:${this.bankConfig.httpPort}/`;
}
get baseUrl(): string {
- return `http://localhost:${this.bankConfig.httpPort}/demobanks/default/access-api`;
+ return this.baseUrlAccessApi;
}
async setSuggestedExchange(
@@ -654,7 +691,11 @@ class LibeufinBankService extends BankServiceBase implements BankService {
await sh(
this.globalTestState,
"libeufin-sandbox-set-default-exchange",
- `libeufin-sandbox default-exchange ${exchangePayto}`
+ `libeufin-sandbox default-exchange ${e.baseUrl} ${exchangePayto}`,
+ {
+ ...process.env,
+ LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
+ },
);
}
@@ -663,38 +704,45 @@ class LibeufinBankService extends BankServiceBase implements BankService {
accountName: string,
password: string,
): Promise<HarnessExchangeBankAccount> {
-
+ console.log("Create Exchange account(s)!");
+ /**
+ * Many test cases try to create a Exchange account before
+ * starting the bank; that's because the Pybank did it entirely
+ * via the configuration file.
+ */
+ await this.start();
+ await this.pingUntilAvailable();
await LibeufinSandboxApi.createDemobankAccount(
accountName,
password,
- { baseUrl: this.baseUrl }
+ { baseUrl: this.baseUrlAccessApi }
);
- let bankAccountLabel = `${accountName}-acct`
+ let bankAccountLabel = accountName;
await LibeufinSandboxApi.createDemobankEbicsSubscriber(
{
- hostID: "talertest-ebics-host",
- userID: "exchange-ebics-user",
- partnerID: "exchange-ebics-partner",
+ hostID: "talertestEbicsHost",
+ userID: "exchangeEbicsUser",
+ partnerID: "exchangeEbicsPartner",
},
bankAccountLabel,
- { baseUrl: this.baseUrl }
+ { baseUrl: this.baseUrlDemobank }
);
await LibeufinNexusApi.createUser(
{ baseUrl: this.nexusBaseUrl },
{
- username: `${accountName}-nexus-username`,
- password: `${password}-nexus-password`
+ username: accountName,
+ password: password
}
);
await LibeufinNexusApi.createEbicsBankConnection(
{ baseUrl: this.nexusBaseUrl },
{
name: "ebics-connection", // connection name.
- ebicsURL: `http://localhost:${this.bankConfig.httpPort}/ebicsweb`,
- hostID: "talertest-ebics-host",
- userID: "exchange-ebics-user",
- partnerID: "exchange-ebics-partner",
+ ebicsURL: (new URL("ebicsweb", this.baseUrlNetloc)).href,
+ hostID: "talertestEbicsHost",
+ userID: "exchangeEbicsUser",
+ partnerID: "exchangeEbicsPartner",
}
);
await LibeufinNexusApi.connectBankConnection(
@@ -706,7 +754,7 @@ class LibeufinBankService extends BankServiceBase implements BankService {
await LibeufinNexusApi.importConnectionAccount(
{ baseUrl: this.nexusBaseUrl },
"ebics-connection", // connection name
- `${accountName}-acct`, // offered account label
+ accountName, // offered account label
`${accountName}-nexus-label` // bank account label at Nexus
);
await LibeufinNexusApi.createTwgFacade(
@@ -724,7 +772,7 @@ class LibeufinBankService extends BankServiceBase implements BankService {
{
action: "grant",
permission: {
- subjectId: `${accountName}-nexus-username`,
+ subjectId: accountName,
subjectType: "user",
resourceType: "facade",
resourceId: "exchange-facade", // facade name
@@ -737,7 +785,7 @@ class LibeufinBankService extends BankServiceBase implements BankService {
{
action: "grant",
permission: {
- subjectId: `${accountName}-nexus-username`,
+ subjectId: accountName,
subjectType: "user",
resourceType: "facade",
resourceId: "exchange-facade", // facade name
@@ -745,12 +793,35 @@ class LibeufinBankService extends BankServiceBase implements BankService {
},
}
);
+ // Set fetch task.
+ await LibeufinNexusApi.postTask(
+ { baseUrl: this.nexusBaseUrl },
+ `${accountName}-nexus-label`,
+ {
+ name: "wirewatch-task",
+ cronspec: "* * *",
+ type: "fetch",
+ params: {
+ level: "all",
+ rangeType: "all",
+ },
+ });
+ await LibeufinNexusApi.postTask(
+ { baseUrl: this.nexusBaseUrl },
+ `${accountName}-nexus-label`,
+ {
+ name: "aggregator-task",
+ cronspec: "* * *",
+ type: "submit",
+ params: {},
+ }
+ );
let facadesResp = await LibeufinNexusApi.getAllFacades({ baseUrl: this.nexusBaseUrl });
let accountInfoResp = await LibeufinSandboxApi.demobankAccountInfo(
- accountName, // username
- password,
- { baseUrl: this.nexusBaseUrl },
- `${accountName}acct` // bank account label.
+ "admin",
+ "secret",
+ { baseUrl: this.baseUrlAccessApi },
+ accountName // bank account label.
);
return {
accountName: accountName,
@@ -761,15 +832,36 @@ class LibeufinBankService extends BankServiceBase implements BankService {
}
async start(): Promise<void> {
- let sandboxDb = `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`;
- let nexusDb = `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`;
+ /**
+ * Because many test cases try to create a Exchange bank
+ * account _before_ starting the bank (Pybank did it only via
+ * the config), it is possible that at this point Sandbox and
+ * Nexus are already running. Hence, this method only launches
+ * them if they weren't launched earlier.
+ */
+
+ // Only go ahead if BOTH aren't running.
+ if (this.sandboxProc || this.nexusProc) {
+ console.log("Nexus or Sandbox already running, not taking any action.");
+ return;
+ }
+ await sh(
+ this.globalTestState,
+ "libeufin-sandbox-config-demobank",
+ `libeufin-sandbox config --currency=${this.bankConfig.currency} default`,
+ {
+ ...process.env,
+ LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
+ LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret",
+ },
+ );
this.sandboxProc = this.globalTestState.spawnService(
"libeufin-sandbox",
["serve", "--port", `${this.port}`],
"libeufin-sandbox",
{
...process.env,
- LIBEUFIN_SANDBOX_DB_CONNECTION: sandboxDb,
+ LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret",
},
);
@@ -780,34 +872,48 @@ class LibeufinBankService extends BankServiceBase implements BankService {
["superuser", "admin", "--password", "test"],
{
...process.env,
- LIBEUFIN_NEXUS_DB_CONNECTION: nexusDb,
+ LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusDbConn,
},
);
-
this.nexusProc = this.globalTestState.spawnService(
"libeufin-nexus",
- ["serve", "--port", `${this.port + 1}`],
+ ["serve", "--port", `${this.nexusPort}`],
"libeufin-nexus",
{
...process.env,
- LIBEUFIN_NEXUS_DB_CONNECTION: nexusDb,
+ LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusDbConn,
},
);
+ // need to wait here, because at this point
+ // a Ebics host needs to be created (RESTfully)
+ await this.pingUntilAvailable();
+ LibeufinSandboxApi.createEbicsHost(
+ { baseUrl: this.baseUrlNetloc },
+ "talertestEbicsHost"
+ );
}
async pingUntilAvailable(): Promise<void> {
- await pingProc(this.sandboxProc, this.baseUrl, "libeufin-sandbox");
- await pingProc(this.nexusProc, `${this.baseUrl}config`, "libeufin-nexus");
+ await pingProc(
+ this.sandboxProc,
+ `http://localhost:${this.bankConfig.httpPort}`,
+ "libeufin-sandbox"
+ );
+ await pingProc(
+ this.nexusProc,
+ `${this.nexusBaseUrl}/config`,
+ "libeufin-nexus"
+ );
}
}
-export class BankService extends BankServiceBase implements BankServiceInterface {
+class PybankService extends BankServiceBase implements BankServiceInterface {
proc: ProcessWrapper | undefined;
static async create(
gc: GlobalTestState,
bc: BankConfig,
- ): Promise<BankService> {
+ ): Promise<PybankService> {
const config = new Configuration();
setTalerPaths(config, gc.testDir + "/talerhome");
config.setString("taler", "currency", bc.currency);
@@ -835,7 +941,7 @@ export class BankService extends BankServiceBase implements BankServiceInterface
`taler-bank-manage -c '${cfgFilename}' django provide_accounts`,
);
- return new BankService(gc, bc, cfgFilename);
+ return new PybankService(gc, bc, cfgFilename);
}
setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) {
@@ -870,7 +976,7 @@ export class BankService extends BankServiceBase implements BankServiceInterface
return {
accountName: accountName,
accountPassword: password,
- accountPaytoUri: `payto://x-taler-bank/${accountName}`,
+ accountPaytoUri: getPayto(accountName),
wireGatewayApiBaseUrl: `http://localhost:${this.bankConfig.httpPort}/taler-wire-gateway/${accountName}/`,
};
}
@@ -893,11 +999,31 @@ export class BankService extends BankServiceBase implements BankServiceInterface
}
}
-// Still work in progress..
-if (false && process.env.WALLET_HARNESS_WITH_EUFIN) {
- BankService.create = LibeufinBankService.create;
- BankService.prototype = Object.create(LibeufinBankService.prototype);
-}
+
+/**
+ * Return a euFin or a pyBank implementation of
+ * the exported BankService class. This allows
+ * to "dynamically export" such class depending
+ * on a particular env variable.
+ */
+function getBankServiceImpl(): {
+ prototype: typeof PybankService.prototype,
+ create: typeof PybankService.create
+} {
+
+ if (process.env.WALLET_HARNESS_WITH_EUFIN)
+ return {
+ prototype: EufinBankService.prototype,
+ create: EufinBankService.create
+ }
+ return {
+ prototype: PybankService.prototype,
+ create: PybankService.create
+ }
+}
+
+export type BankService = PybankService;
+export const BankService = getBankServiceImpl();
export class FakeBankService {
proc: ProcessWrapper | undefined;
@@ -1038,6 +1164,10 @@ export class ExchangeService implements ExchangeServiceInterface {
}
async runWirewatchOnce() {
+ if (process.env.WALLET_HARNESS_WITH_EUFIN) {
+ // Not even 2 secods showed to be enough!
+ await waitMs(4000);
+ }
await runCommand(
this.globalState,
`exchange-${this.name}-wirewatch-once`,
@@ -1699,7 +1829,7 @@ export class MerchantService implements MerchantServiceInterface {
return await this.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
auth: {
method: "external",
},
@@ -1956,3 +2086,46 @@ export class WalletCli {
);
}
}
+
+export function getRandomIban(salt: string | null = null): string {
+
+ function getBban(salt: string | null): string {
+ if (!salt)
+ return Math.random().toString().substring(2, 6);
+ let hashed = hash(stringToBytes(salt));
+ let ret = "";
+ for (let i = 0; i < hashed.length; i++) {
+ ret += hashed[i].toString();
+ }
+ return ret.substring(0, 4);
+ }
+
+ let cc_no_check = "131400"; // == DE00
+ let bban = getBban(salt)
+ let check_digits = (98 - (Number.parseInt(`${bban}${cc_no_check}`) % 97)).toString();
+ if (check_digits.length == 1) {
+ check_digits = `0${check_digits}`;
+ }
+ return `DE${check_digits}${bban}`;
+}
+
+// Only used in one tipping test.
+export function getWireMethod(): string {
+ if (process.env.WALLET_HARNESS_WITH_EUFIN)
+ return "iban"
+ return "x-taler-bank"
+}
+
+/**
+ * Generate a payto address, whose authority depends
+ * on whether the banking is served by euFin or Pybank.
+ */
+export function getPayto(label: string): string {
+ if (process.env.WALLET_HARNESS_WITH_EUFIN)
+ return `payto://iban/SANDBOXX/${getRandomIban(label)}?receiver-name=${label}`
+ return `payto://x-taler-bank/${label}`
+}
+
+function waitMs(ms: number): Promise<void> {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
diff --git a/packages/taler-wallet-cli/src/harness/helpers.ts b/packages/taler-wallet-cli/src/harness/helpers.ts
index 6ff62504b..bac2eefc1 100644
--- a/packages/taler-wallet-cli/src/harness/helpers.ts
+++ b/packages/taler-wallet-cli/src/harness/helpers.ts
@@ -50,6 +50,7 @@ import {
MerchantPrivateApi,
HarnessExchangeBankAccount,
WithAuthorization,
+ getPayto
} from "./harness.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -94,7 +95,7 @@ export async function createSimpleTestkudosEnvironment(
});
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
@@ -118,13 +119,13 @@ export async function createSimpleTestkudosEnvironment(
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
await merchant.addInstance({
id: "minst1",
name: "minst1",
- paytoUris: ["payto://x-taler-bank/minst1"],
+ paytoUris: [getPayto("minst1")],
});
console.log("setup done!");
@@ -186,7 +187,7 @@ export async function createFaultInjectedMerchantTestkudosEnvironment(
const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081);
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
@@ -213,13 +214,13 @@ export async function createFaultInjectedMerchantTestkudosEnvironment(
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
await merchant.addInstance({
id: "minst1",
name: "minst1",
- paytoUris: ["payto://x-taler-bank/minst1"],
+ paytoUris: [getPayto("minst1")],
});
console.log("setup done!");
@@ -263,16 +264,19 @@ export async function startWithdrawViaBank(
await wallet.runPending();
- // Confirm it
-
- await BankApi.confirmWithdrawalOperation(bank, user, wop);
-
- // Withdraw
+ // Withdraw (AKA select)
await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
});
+
+ // Confirm it
+
+ await BankApi.confirmWithdrawalOperation(bank, user, wop);
+
+ await wallet.runPending();
+ await wallet.runUntilDone();
}
/**
diff --git a/packages/taler-wallet-cli/src/harness/libeufin-apis.ts b/packages/taler-wallet-cli/src/harness/libeufin-apis.ts
index 68a25d92f..13d27c467 100644
--- a/packages/taler-wallet-cli/src/harness/libeufin-apis.ts
+++ b/packages/taler-wallet-cli/src/harness/libeufin-apis.ts
@@ -184,7 +184,7 @@ export namespace LibeufinSandboxApi {
libeufinSandboxService: LibeufinSandboxServiceInterface,
accountLabel: string
) {
- let url = new URL(`${libeufinSandboxService.baseUrl}/accounts/${accountLabel}`);
+ let url = new URL(`accounts/${accountLabel}`,libeufinSandboxService.baseUrl);
return await axios.get(url.href, {
auth: {
username: username,
@@ -199,7 +199,7 @@ export namespace LibeufinSandboxApi {
password: string,
libeufinSandboxService: LibeufinSandboxServiceInterface,
) {
- let url = new URL(`${libeufinSandboxService.baseUrl}/testing/register`);
+ let url = new URL("testing/register", libeufinSandboxService.baseUrl);
await axios.post(url.href, {
username: username,
password: password
@@ -214,11 +214,11 @@ export namespace LibeufinSandboxApi {
password: string = "secret",
) {
// baseUrl should already be pointed to one demobank.
- let url = new URL(libeufinSandboxService.baseUrl);
+ let url = new URL("ebics/subscribers", libeufinSandboxService.baseUrl);
await axios.post(url.href, {
userID: req.userID,
hostID: req.hostID,
- partnerID: req.userID,
+ partnerID: req.partnerID,
demobankAccountLabel: demobankAccountLabel,
}, {
auth: {
diff --git a/packages/taler-wallet-cli/src/harness/libeufin.ts b/packages/taler-wallet-cli/src/harness/libeufin.ts
index d101efa52..0107d5a8b 100644
--- a/packages/taler-wallet-cli/src/harness/libeufin.ts
+++ b/packages/taler-wallet-cli/src/harness/libeufin.ts
@@ -36,8 +36,8 @@ import {
runCommand,
setupDb,
sh,
+ getRandomIban
} from "../harness/harness.js";
-
import {
LibeufinSandboxApi,
LibeufinNexusApi,
@@ -183,10 +183,6 @@ export interface LibeufinPreparedPaymentDetails {
nexusBankAccountName: string;
}
-function getRandomIban(countryCode: string): string {
- return `${countryCode}715001051796${(Math.random().toString().substring(2, 8))}`
-}
-
export class LibeufinSandboxService implements LibeufinSandboxServiceInterface {
static async create(
gc: GlobalTestState,
@@ -405,7 +401,7 @@ export class SandboxUserBundle {
constructor(salt: string) {
this.ebicsBankAccount = {
bic: "BELADEBEXXX",
- iban: getRandomIban("DE"),
+ iban: getRandomIban(),
label: `remote-account-${salt}`,
name: `Taler Exchange: ${salt}`,
subscriber: {
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts b/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts
index 0f8af05e5..2259dd8bb 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts
@@ -27,6 +27,7 @@ import {
BankApi,
BankAccessApi,
CreditDebitIndicator,
+ getPayto
} from "../harness/harness.js";
import { createEddsaKeyPair, encodeCrock } from "@gnu-taler/taler-util";
import { defaultCoinConfig } from "../harness/denomStructures";
@@ -61,7 +62,7 @@ export async function runBankApiTest(t: GlobalTestState) {
});
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
@@ -85,13 +86,13 @@ export async function runBankApiTest(t: GlobalTestState) {
await merchant.addInstance({
id: "minst1",
name: "minst1",
- paytoUris: ["payto://x-taler-bank/minst1"],
+ paytoUris: [getPayto("minst1")],
});
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
console.log("setup done!");
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts b/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts
index f33c8338b..07382c43e 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState } from "../harness/harness.js";
+import { GlobalTestState, getPayto } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
/**
@@ -44,7 +44,7 @@ export async function runDepositTest(t: GlobalTestState) {
WalletApiOperation.CreateDepositGroup,
{
amount: "TESTKUDOS:10",
- depositPaytoUri: "payto://x-taler-bank/localhost/foo",
+ depositPaytoUri: getPayto("foo"),
},
);
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts
index 8a5d563ce..91e9bdec5 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts
@@ -26,6 +26,7 @@ import {
MerchantService,
BankApi,
BankAccessApi,
+ getPayto
} from "../harness/harness.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
@@ -69,7 +70,7 @@ export async function runExchangeManagementTest(t: GlobalTestState) {
});
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
@@ -98,13 +99,13 @@ export async function runExchangeManagementTest(t: GlobalTestState) {
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
await merchant.addInstance({
id: "minst1",
name: "minst1",
- paytoUris: ["payto://x-taler-bank/minst1"],
+ paytoUris: [getPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts
index 56684f70a..9badfd501 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts
@@ -40,6 +40,7 @@ import {
MerchantService,
setupDb,
WalletCli,
+ getPayto
} from "../harness/harness.js";
import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js";
@@ -103,7 +104,7 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
});
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
@@ -127,13 +128,13 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) {
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
await merchant.addInstance({
id: "minst1",
name: "minst1",
- paytoUris: ["payto://x-taler-bank/minst1"],
+ paytoUris: [getPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts b/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts
index 025e12226..d3ff89ae4 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts
@@ -25,6 +25,7 @@ import {
MerchantService,
setupDb,
WalletCli,
+ getPayto
} from "../harness/harness.js";
import {
withdrawViaBank,
@@ -63,7 +64,7 @@ export async function createMyTestkudosEnvironment(
});
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
@@ -140,7 +141,7 @@ export async function createMyTestkudosEnvironment(
await merchant.addInstance({
id: "minst1",
name: "minst1",
- paytoUris: ["payto://x-taler-bank/minst1"],
+ paytoUris: [getPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts
index 8e8f966b9..1e958fd73 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts
@@ -25,6 +25,7 @@ import {
MerchantService,
setupDb,
WalletCli,
+ getPayto
} from "../harness/harness.js";
import {
withdrawViaBank,
@@ -80,7 +81,7 @@ export async function createConfusedMerchantTestkudosEnvironment(
const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081);
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
@@ -108,13 +109,13 @@ export async function createConfusedMerchantTestkudosEnvironment(
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
await merchant.addInstance({
id: "minst1",
name: "minst1",
- paytoUris: ["payto://x-taler-bank/minst1"],
+ paytoUris: [getPayto("minst1")]
});
console.log("setup done!");
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts
index 589c79120..ef926c4af 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts
@@ -25,6 +25,7 @@ import {
MerchantApiClient,
MerchantService,
setupDb,
+ getPayto
} from "../harness/harness.js";
/**
@@ -74,7 +75,7 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
auth: {
method: "external",
},
@@ -84,7 +85,7 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
await merchant.addInstance({
id: "myinst",
name: "Second Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
auth: {
method: "external",
},
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts
index fc5e7305a..6f76e2325 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts
@@ -24,6 +24,7 @@ import {
MerchantApiClient,
MerchantService,
setupDb,
+ getPayto
} from "../harness/harness.js";
/**
@@ -71,7 +72,7 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
default_wire_transfer_delay: { d_ms: 60000 },
jurisdiction: {},
name: "My Default Instance",
- payto_uris: ["payto://x-taler-bank/foo/bar"],
+ payto_uris: [getPayto("bar")],
auth: {
method: "token",
token: "secret-token:i-am-default",
@@ -88,7 +89,7 @@ export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
default_wire_transfer_delay: { d_ms: 60000 },
jurisdiction: {},
name: "My Second Instance",
- payto_uris: ["payto://x-taler-bank/foo/bar"],
+ payto_uris: [getPayto("bar")],
auth: {
method: "token",
token: "secret-token:i-am-myinst",
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts
index 46af87922..1bf6be4cd 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts
@@ -25,6 +25,7 @@ import {
MerchantApiClient,
MerchantService,
setupDb,
+ getPayto
} from "../harness/harness.js";
/**
@@ -74,7 +75,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
auth: {
method: "external",
},
@@ -84,7 +85,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
await merchant.addInstance({
id: "myinst",
name: "Second Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
auth: {
method: "external",
},
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts
index 2be01d919..7e421cc35 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts
@@ -31,6 +31,7 @@ import {
MerchantPrivateApi,
BankApi,
BankAccessApi,
+ getPayto
} from "../harness/harness.js";
import {
FaultInjectedExchangeService,
@@ -64,7 +65,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
});
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
@@ -107,7 +108,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
console.log("setup done!");
@@ -131,18 +132,21 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
await wallet.runPending();
- // Confirm it
-
- await BankApi.confirmWithdrawalOperation(bank, user, wop);
-
// Withdraw
await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
exchangeBaseUrl: faultyExchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
});
+ await wallet.runPending();
+
+ // Confirm it
+
+ await BankApi.confirmWithdrawalOperation(bank, user, wop);
+
await wallet.runUntilDone();
+
// Check balance
await wallet.client.call(WalletApiOperation.GetBalances, {});
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts
index 754c3a0e8..3084ecfe0 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts
@@ -25,6 +25,7 @@ import {
MerchantService,
WalletCli,
MerchantPrivateApi,
+ getPayto
} from "../harness/harness.js";
import { withdrawViaBank } from "../harness/helpers.js";
import { coin_ct10, coin_u1 } from "../harness/denomStructures";
@@ -54,7 +55,7 @@ async function setupTest(
});
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
@@ -86,13 +87,13 @@ async function setupTest(
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
await merchant.addInstance({
id: "minst1",
name: "minst1",
- paytoUris: ["payto://x-taler-bank/minst1"],
+ paytoUris: [getPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts b/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts
index 276c532b5..87c4d958b 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts
@@ -27,6 +27,7 @@ import {
setupDb,
BankService,
delayMs,
+ getPayto
} from "../harness/harness.js";
import {
withdrawViaBank,
@@ -84,7 +85,7 @@ async function createTestEnvironment(
});
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
@@ -121,13 +122,13 @@ async function createTestEnvironment(
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
await merchant.addInstance({
id: "minst1",
name: "minst1",
- paytoUris: ["payto://x-taler-bank/minst1"],
+ paytoUris: [getPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts
index e20d8bdad..b55be9f82 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts
@@ -36,6 +36,7 @@ import {
MerchantService,
setupDb,
WalletCli,
+ getPayto
} from "../harness/harness.js";
import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js";
@@ -97,7 +98,7 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
});
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
@@ -121,13 +122,13 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) {
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
await merchant.addInstance({
id: "minst1",
name: "minst1",
- paytoUris: ["payto://x-taler-bank/minst1"],
+ paytoUris: [getPayto("minst1")],
});
console.log("setup done!");
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts
index c6a7f8402..f31220e24 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts
@@ -18,7 +18,7 @@
* Imports.
*/
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, MerchantPrivateApi, BankApi } from "../harness/harness.js";
+import { GlobalTestState, MerchantPrivateApi, BankApi, getWireMethod } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
/**
@@ -43,7 +43,7 @@ export async function runTippingTest(t: GlobalTestState) {
{
exchange_url: exchange.baseUrl,
initial_balance: "TESTKUDOS:10",
- wire_method: "x-taler-bank",
+ wire_method: getWireMethod(),
},
);
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts
index c21a7279b..c42ae5adf 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts
@@ -32,6 +32,7 @@ import {
MerchantService,
setupDb,
WalletCli,
+ getPayto
} from "../harness/harness.js";
import { SimpleTestEnvironment } from "../harness/helpers.js";
@@ -69,7 +70,7 @@ export async function createMyEnvironment(
});
const exchangeBankAccount = await bank.createExchangeAccount(
- "MyExchange",
+ "myexchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
@@ -93,7 +94,7 @@ export async function createMyEnvironment(
await merchant.addInstance({
id: "default",
name: "Default Instance",
- paytoUris: [`payto://x-taler-bank/merchant-default`],
+ paytoUris: [getPayto("merchant-default")],
});
console.log("setup done!");
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts
index fe719ea62..5ba1fa893 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts
@@ -47,12 +47,18 @@ export async function runWithdrawalAbortBankTest(t: GlobalTestState) {
await wallet.runPending();
- // Confirm it
+ // Abort it
await BankApi.abortWithdrawalOperation(bank, user, wop);
// Withdraw
+ // Difference:
+ // -> with euFin, the wallet selects
+ // -> with PyBank, the wallet stops _before_
+ //
+ // WHY ?!
+ //
const e = await t.assertThrowsOperationErrorAsync(async () => {
await wallet.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts
index 35969c78f..25df19e46 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts
@@ -47,16 +47,18 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
await wallet.runPending();
- // Confirm it
-
- await BankApi.confirmWithdrawalOperation(bank, user, wop);
-
// Withdraw
const r2 = await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
});
+ await wallet.runPending();
+
+ // Confirm it
+
+ await BankApi.confirmWithdrawalOperation(bank, user, wop);
+
await wallet.runUntilDone();
// Check balance
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts
index b93d1b500..2f88b3024 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts
@@ -50,6 +50,7 @@ export async function runTestWithdrawalManualTest(t: GlobalTestState) {
const reservePub: string = wres.reservePub;
+ // Bug.
await BankApi.adminAddIncoming(bank, {
exchangeBankAccount,
amount: "TESTKUDOS:10",
diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json
index d8b344f2c..3f20811ff 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -58,7 +58,7 @@
"rollup-plugin-sourcemaps": "^0.6.3",
"source-map-resolve": "^0.6.0",
"typedoc": "^0.20.16",
- "typescript": "^4.1.3"
+ "typescript": "^4.4.4"
},
"dependencies": {
"@gnu-taler/idb-bridge": "workspace:*",
diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
index 922fbbfac..7d616ecb6 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
@@ -27,13 +27,13 @@
/**
* Imports.
*/
-import { AmountJson } from "@gnu-taler/taler-util";
+import { AmountJson, DenominationPubKey } from "@gnu-taler/taler-util";
export interface RefreshNewDenomInfo {
count: number;
value: AmountJson;
feeWithdraw: AmountJson;
- denomPub: string;
+ denomPub: DenominationPubKey;
}
/**
@@ -117,7 +117,7 @@ export interface DerivedRefreshSession {
export interface DeriveTipRequest {
secretSeed: string;
- denomPub: string;
+ denomPub: DenominationPubKey;
planchetIndex: number;
}
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
index 6bace01a3..e6c0290f1 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
@@ -24,7 +24,7 @@
*/
import { CoinRecord, DenominationRecord, WireFee } from "../../db.js";
-import { CryptoWorker } from "./cryptoWorker.js";
+import { CryptoWorker } from "./cryptoWorkerInterface.js";
import { RecoupRequest, CoinDepositPermission } from "@gnu-taler/taler-util";
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
index c42ece778..389b98b22 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
@@ -15,9 +15,7 @@
*/
/**
- * Synchronous implementation of crypto-related functions for the wallet.
- *
- * The functionality is parameterized over an Emscripten environment.
+ * Implementation of crypto-related high-level functions for the Taler wallet.
*
* @author Florian Dold <dold@taler.net>
*/
@@ -37,9 +35,11 @@ import {
import {
buildSigPS,
CoinDepositPermission,
+ DenomKeyType,
+ FreshCoin,
+ hashDenomPub,
RecoupRequest,
RefreshPlanchetInfo,
- SignaturePurposeBuilder,
TalerSignaturePurpose,
} from "@gnu-taler/taler-util";
// FIXME: These types should be internal to the wallet!
@@ -128,25 +128,46 @@ function timestampRoundedToBuffer(ts: Timestamp): Uint8Array {
return new Uint8Array(b);
}
+export interface PrimitiveWorker {
+ setupRefreshPlanchet(arg0: {
+ transfer_secret: string;
+ coin_index: number;
+ }): Promise<{
+ coin_pub: string;
+ coin_priv: string;
+ blinding_key: string;
+ }>;
+ eddsaVerify(req: {
+ msg: string;
+ sig: string;
+ pub: string;
+ }): Promise<{ valid: boolean }>;
+}
+
export class CryptoImplementation {
static enableTracing = false;
+ constructor(private primitiveWorker?: PrimitiveWorker) {}
+
/**
* Create a pre-coin of the given denomination to be withdrawn from then given
* reserve.
*/
createPlanchet(req: PlanchetCreationRequest): PlanchetCreationResult {
+ if (req.denomPub.cipher !== 1) {
+ throw Error("unsupported cipher");
+ }
const reservePub = decodeCrock(req.reservePub);
const reservePriv = decodeCrock(req.reservePriv);
- const denomPub = decodeCrock(req.denomPub);
+ const denomPubRsa = decodeCrock(req.denomPub.rsa_public_key);
const derivedPlanchet = setupWithdrawPlanchet(
decodeCrock(req.secretSeed),
req.coinIndex,
);
const coinPubHash = hash(derivedPlanchet.coinPub);
- const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPub);
+ const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPubRsa);
const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount;
- const denomPubHash = hash(denomPub);
+ const denomPubHash = hashDenomPub(req.denomPub);
const evHash = hash(ev);
const withdrawRequest = buildSigPS(
@@ -166,7 +187,10 @@ export class CryptoImplementation {
coinPriv: encodeCrock(derivedPlanchet.coinPriv),
coinPub: encodeCrock(derivedPlanchet.coinPub),
coinValue: req.value,
- denomPub: encodeCrock(denomPub),
+ denomPub: {
+ cipher: 1,
+ rsa_public_key: encodeCrock(denomPubRsa),
+ },
denomPubHash: encodeCrock(denomPubHash),
reservePub: encodeCrock(reservePub),
withdrawSig: encodeCrock(sig),
@@ -179,8 +203,11 @@ export class CryptoImplementation {
* Create a planchet used for tipping, including the private keys.
*/
createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet {
+ if (req.denomPub.cipher !== 1) {
+ throw Error("unsupported cipher");
+ }
const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex);
- const denomPub = decodeCrock(req.denomPub);
+ const denomPub = decodeCrock(req.denomPub.rsa_public_key);
const coinPubHash = hash(fc.coinPub);
const ev = rsaBlind(coinPubHash, fc.bks, denomPub);
@@ -246,7 +273,11 @@ export class CryptoImplementation {
/**
* Check if a wire fee is correctly signed.
*/
- isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean {
+ async isValidWireFee(
+ type: string,
+ wf: WireFee,
+ masterPub: string,
+ ): Promise<boolean> {
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_FEES)
.put(hash(stringToBytes(type + "\0")))
.put(timestampRoundedToBuffer(wf.startStamp))
@@ -256,13 +287,25 @@ export class CryptoImplementation {
.build();
const sig = decodeCrock(wf.sig);
const pub = decodeCrock(masterPub);
+ if (this.primitiveWorker) {
+ return (
+ await this.primitiveWorker.eddsaVerify({
+ msg: encodeCrock(p),
+ pub: masterPub,
+ sig: encodeCrock(sig),
+ })
+ ).valid;
+ }
return eddsaVerify(p, sig, pub);
}
/**
* Check if the signature of a denomination is valid.
*/
- isValidDenom(denom: DenominationRecord, masterPub: string): boolean {
+ async isValidDenom(
+ denom: DenominationRecord,
+ masterPub: string,
+ ): Promise<boolean> {
const p = buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY)
.put(decodeCrock(masterPub))
.put(timestampRoundedToBuffer(denom.stampStart))
@@ -287,14 +330,9 @@ export class CryptoImplementation {
sig: string,
masterPub: string,
): boolean {
- const h = kdf(
- 64,
- stringToBytes("exchange-wire-signature"),
- stringToBytes(paytoUri + "\0"),
- new Uint8Array(0),
- );
+ const paytoHash = hash(stringToBytes(paytoUri + "\0"));
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
- .put(h)
+ .put(paytoHash)
.build();
return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
}
@@ -353,8 +391,11 @@ export class CryptoImplementation {
* and deposit permissions for each given coin.
*/
signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
+ // FIXME: put extensions here if used
+ const hExt = new Uint8Array(64);
const d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
.put(decodeCrock(depositInfo.contractTermsHash))
+ .put(hExt)
.put(decodeCrock(depositInfo.wireInfoHash))
.put(decodeCrock(depositInfo.denomPubHash))
.put(timestampRoundedToBuffer(depositInfo.timestamp))
@@ -362,7 +403,6 @@ export class CryptoImplementation {
.put(amountToBuffer(depositInfo.spendAmount))
.put(amountToBuffer(depositInfo.feeDeposit))
.put(decodeCrock(depositInfo.merchantPub))
- .put(decodeCrock(depositInfo.coinPub))
.build();
const coinSig = eddsaSign(d, decodeCrock(depositInfo.coinPriv));
@@ -372,14 +412,17 @@ export class CryptoImplementation {
contribution: Amounts.stringify(depositInfo.spendAmount),
h_denom: depositInfo.denomPubHash,
exchange_url: depositInfo.exchangeBaseUrl,
- ub_sig: depositInfo.denomSig,
+ ub_sig: {
+ cipher: DenomKeyType.Rsa,
+ rsa_signature: depositInfo.denomSig.rsa_signature,
+ },
};
return s;
}
- deriveRefreshSession(
+ async deriveRefreshSession(
req: DeriveRefreshSessionRequest,
- ): DerivedRefreshSession {
+ ): Promise<DerivedRefreshSession> {
const {
newCoinDenoms,
feeRefresh: meltFee,
@@ -423,8 +466,10 @@ export class CryptoImplementation {
for (const denomSel of newCoinDenoms) {
for (let i = 0; i < denomSel.count; i++) {
- const r = decodeCrock(denomSel.denomPub);
- sessionHc.update(r);
+ if (denomSel.denomPub.cipher !== 1) {
+ throw Error("unsupported cipher");
+ }
+ sessionHc.update(hashDenomPub(denomSel.denomPub));
}
}
@@ -435,19 +480,38 @@ export class CryptoImplementation {
for (let j = 0; j < newCoinDenoms.length; j++) {
const denomSel = newCoinDenoms[j];
for (let k = 0; k < denomSel.count; k++) {
- const coinNumber = planchets.length;
+ const coinIndex = planchets.length;
const transferPriv = decodeCrock(transferPrivs[i]);
const oldCoinPub = decodeCrock(meltCoinPub);
const transferSecret = keyExchangeEcdheEddsa(
transferPriv,
oldCoinPub,
);
- const fresh = setupRefreshPlanchet(transferSecret, coinNumber);
- const coinPriv = fresh.coinPriv;
- const coinPub = fresh.coinPub;
- const blindingFactor = fresh.bks;
+ let coinPub: Uint8Array;
+ let coinPriv: Uint8Array;
+ let blindingFactor: Uint8Array;
+ if (this.primitiveWorker) {
+ const r = await this.primitiveWorker.setupRefreshPlanchet({
+ transfer_secret: encodeCrock(transferSecret),
+ coin_index: coinIndex,
+ });
+ coinPub = decodeCrock(r.coin_pub);
+ coinPriv = decodeCrock(r.coin_priv);
+ blindingFactor = decodeCrock(r.blinding_key);
+ } else {
+ let fresh: FreshCoin = setupRefreshPlanchet(
+ transferSecret,
+ coinIndex,
+ );
+ coinPriv = fresh.coinPriv;
+ coinPub = fresh.coinPub;
+ blindingFactor = fresh.bks;
+ }
const pubHash = hash(coinPub);
- const denomPub = decodeCrock(denomSel.denomPub);
+ if (denomSel.denomPub.cipher !== 1) {
+ throw Error("unsupported cipher");
+ }
+ const denomPub = decodeCrock(denomSel.denomPub.rsa_public_key);
const ev = rsaBlind(pubHash, blindingFactor, denomPub);
const planchet: RefreshPlanchetInfo = {
blindingKey: encodeCrock(blindingFactor),
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoWorkerInterface.ts
index 9f3ee6f50..9f3ee6f50 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoWorkerInterface.ts
diff --git a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts
index 3f7f9e170..df57635d1 100644
--- a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts
@@ -18,7 +18,7 @@
* Imports
*/
import { CryptoWorkerFactory } from "./cryptoApi.js";
-import { CryptoWorker } from "./cryptoWorker.js";
+import { CryptoWorker } from "./cryptoWorkerInterface.js";
import os from "os";
import { CryptoImplementation } from "./cryptoImplementation.js";
import { Logger } from "@gnu-taler/taler-util";
@@ -94,7 +94,7 @@ export function handleWorkerMessage(msg: any): void {
}
try {
- const result = (impl as any)[operation](...args);
+ const result = await (impl as any)[operation](...args);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const _r = "require";
const worker_threads: typeof import("worker_threads") = module[_r](
diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts
index f6b8ac5d7..8293bb369 100644
--- a/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts
@@ -14,10 +14,107 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { CryptoImplementation } from "./cryptoImplementation.js";
+import {
+ CryptoImplementation,
+ PrimitiveWorker,
+} from "./cryptoImplementation.js";
import { CryptoWorkerFactory } from "./cryptoApi.js";
-import { CryptoWorker } from "./cryptoWorker.js";
+import { CryptoWorker } from "./cryptoWorkerInterface.js";
+
+import child_process from "child_process";
+import type internal from "stream";
+import { OpenedPromise, openPromise } from "../../index.js";
+import { FreshCoin, Logger } from "@gnu-taler/taler-util";
+
+const logger = new Logger("synchronousWorker.ts");
+
+class MyPrimitiveWorker implements PrimitiveWorker {
+ proc: child_process.ChildProcessByStdio<
+ internal.Writable,
+ internal.Readable,
+ null
+ >;
+ requests: Array<{
+ p: OpenedPromise<any>;
+ req: any;
+ }> = [];
+
+ constructor() {
+ const stdoutChunks: Buffer[] = [];
+ this.proc = child_process.spawn("taler-crypto-worker", {
+ //stdio: ["pipe", "pipe", "inherit"],
+ stdio: ["pipe", "pipe", "inherit"],
+ detached: true,
+ });
+ this.proc.on("close", function (code) {
+ logger.error("child process exited");
+ });
+ (this.proc.stdout as any).unref();
+ (this.proc.stdin as any).unref();
+ this.proc.unref();
+
+ this.proc.stdout.on("data", (x) => {
+ // console.log("got chunk", x.toString("utf-8"));
+ if (x instanceof Buffer) {
+ const nlIndex = x.indexOf("\n");
+ if (nlIndex >= 0) {
+ const before = x.slice(0, nlIndex);
+ const after = x.slice(nlIndex + 1);
+ stdoutChunks.push(after);
+ const str = Buffer.concat([...stdoutChunks, before]).toString(
+ "utf-8",
+ );
+ const req = this.requests.shift()!;
+ if (this.requests.length === 0) {
+ this.proc.unref();
+ }
+ //logger.info(`got response: ${str}`);
+ req.p.resolve(JSON.parse(str));
+ } else {
+ stdoutChunks.push(x);
+ }
+ } else {
+ throw Error(`unexpected data chunk type (${typeof x})`);
+ }
+ });
+ }
+
+ async setupRefreshPlanchet(req: {
+ transfer_secret: string;
+ coin_index: number;
+ }): Promise<{
+ coin_pub: string;
+ coin_priv: string;
+ blinding_key: string;
+ }> {
+ return this.queueRequest({
+ op: "setup_refresh_planchet",
+ args: req,
+ });
+ }
+
+ async queueRequest(req: any): Promise<any> {
+ const p = openPromise<any>();
+ if (this.requests.length === 0) {
+ this.proc.ref();
+ }
+ this.requests.push({ req, p });
+ this.proc.stdin.write(JSON.stringify(req) + "\n");
+ return p.promise;
+ }
+
+ async eddsaVerify(req: {
+ msg: string;
+ sig: string;
+ pub: string;
+ }): Promise<{ valid: boolean }> {
+ return this.queueRequest({
+ op: "eddsa_verify",
+ args: req,
+ });
+ }
+}
/**
* The synchronous crypto worker produced by this factory doesn't run in the
@@ -50,9 +147,14 @@ export class SynchronousCryptoWorker {
*/
onerror: undefined | ((m: any) => void);
+ primitiveWorker: PrimitiveWorker;
+
constructor() {
this.onerror = undefined;
this.onmessage = undefined;
+ if (process.env["TALER_WALLET_PRIMITIVE_WORKER"]) {
+ this.primitiveWorker = new MyPrimitiveWorker();
+ }
}
/**
@@ -80,7 +182,7 @@ export class SynchronousCryptoWorker {
id: number,
args: string[],
): Promise<void> {
- const impl = new CryptoImplementation();
+ const impl = new CryptoImplementation(this.primitiveWorker);
if (!(operation in impl)) {
console.error(`crypto operation '${operation}' not found`);
@@ -89,16 +191,16 @@ export class SynchronousCryptoWorker {
let result: any;
try {
- result = (impl as any)[operation](...args);
+ result = await (impl as any)[operation](...args);
} catch (e) {
- console.log("error during operation", e);
+ logger.error("error during operation", e);
return;
}
try {
setTimeout(() => this.dispatchMessage({ result, id }), 0);
} catch (e) {
- console.log("got error during dispatch", e);
+ logger.error("got error during dispatch", e);
}
}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 902f749cf..483cb16c2 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -28,6 +28,7 @@ import {
Auditor,
CoinDepositPermission,
ContractTerms,
+ DenominationPubKey,
Duration,
ExchangeSignKeyJson,
InternationalizedString,
@@ -36,6 +37,7 @@ import {
RefreshReason,
TalerErrorDetails,
Timestamp,
+ UnblindedSignature,
} from "@gnu-taler/taler-util";
import { RetryInfo } from "./util/retries.js";
import { PayCoinSelection } from "./util/coinSelection.js";
@@ -310,7 +312,7 @@ export interface DenominationRecord {
/**
* The denomination public key.
*/
- denomPub: string;
+ denomPub: DenominationPubKey;
/**
* Hash of the denomination public key.
@@ -452,7 +454,7 @@ export interface ExchangeDetailsRecord {
/**
* content-type of the last downloaded termsOfServiceText.
*/
- termsOfServiceContentType: string | undefined;
+ termsOfServiceContentType: string | undefined;
/**
* ETag for last terms of service download.
@@ -578,7 +580,8 @@ export interface PlanchetRecord {
denomPubHash: string;
- denomPub: string;
+ // FIXME: maybe too redundant?
+ denomPub: DenominationPubKey;
blindingKey: string;
@@ -668,7 +671,7 @@ export interface CoinRecord {
/**
* Key used by the exchange used to sign the coin.
*/
- denomPub: string;
+ denomPub: DenominationPubKey;
/**
* Hash of the public key that signs the coin.
@@ -678,7 +681,7 @@ export interface CoinRecord {
/**
* Unblinded signature by the exchange.
*/
- denomSig: string;
+ denomSig: UnblindedSignature;
/**
* Amount that's left on the coin.
diff --git a/packages/taler-wallet-core/src/errors.ts b/packages/taler-wallet-core/src/errors.ts
index d788405ff..3109644ac 100644
--- a/packages/taler-wallet-core/src/errors.ts
+++ b/packages/taler-wallet-core/src/errors.ts
@@ -93,7 +93,7 @@ export async function guardOperationException<T>(
): Promise<T> {
try {
return await op();
- } catch (e) {
+ } catch (e: any) {
if (e instanceof OperationFailedAndReportedError) {
throw e;
}
diff --git a/packages/taler-wallet-core/src/headless/helpers.ts b/packages/taler-wallet-core/src/headless/helpers.ts
index f2285e149..191c48441 100644
--- a/packages/taler-wallet-core/src/headless/helpers.ts
+++ b/packages/taler-wallet-core/src/headless/helpers.ts
@@ -142,19 +142,23 @@ export async function getDefaultNodeWallet(
const myDb = await openTalerDatabase(myIdbFactory, myVersionChange);
let workerFactory;
- try {
- // Try if we have worker threads available, fails in older node versions.
- const _r = "require";
- const worker_threads = module[_r]("worker_threads");
- // require("worker_threads");
- workerFactory = new NodeThreadCryptoWorkerFactory();
- } catch (e) {
- logger.warn(
- "worker threads not available, falling back to synchronous workers",
- );
+ if (process.env["TALER_WALLET_SYNC_CRYPTO"]) {
+ logger.info("using synchronous crypto worker");
workerFactory = new SynchronousCryptoWorkerFactory();
+ } else {
+ try {
+ // Try if we have worker threads available, fails in older node versions.
+ const _r = "require";
+ const worker_threads = module[_r]("worker_threads");
+ // require("worker_threads");
+ workerFactory = new NodeThreadCryptoWorkerFactory();
+ } catch (e) {
+ logger.warn(
+ "worker threads not available, falling back to synchronous workers",
+ );
+ workerFactory = new SynchronousCryptoWorkerFactory();
+ }
}
-
const w = await Wallet.create(myDb, myHttpLib, workerFactory);
if (args.notifyHandler) {
diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts
index 0b360a248..5489bd5a3 100644
--- a/packages/taler-wallet-core/src/index.ts
+++ b/packages/taler-wallet-core/src/index.ts
@@ -34,7 +34,7 @@ export * from "./db-utils.js";
// Crypto and crypto workers
// export * from "./crypto/workers/nodeThreadWorker.js";
export { CryptoImplementation } from "./crypto/workers/cryptoImplementation.js";
-export type { CryptoWorker } from "./crypto/workers/cryptoWorker.js";
+export type { CryptoWorker } from "./crypto/workers/cryptoWorkerInterface.js";
export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi.js";
export * from "./pending-types.js";
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index 7623ab189..e8e1de0b9 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -202,7 +202,7 @@ export interface CompletedCoin {
* as the async crypto worker communication would auto-close the database transaction.
*/
export interface BackupCryptoPrecomputedData {
- denomPubToHash: Record<string, string>;
+ rsaDenomPubToHash: Record<string, string>;
coinPrivToCompletedCoin: Record<string, CompletedCoin>;
proposalNoncePrivToPub: { [priv: string]: string };
proposalIdToContractTermsHash: { [proposalId: string]: string };
@@ -330,8 +330,13 @@ export async function importBackup(
}
for (const backupDenomination of backupExchangeDetails.denominations) {
+ if (backupDenomination.denom_pub.cipher !== 1) {
+ throw Error("unsupported cipher");
+ }
const denomPubHash =
- cryptoComp.denomPubToHash[backupDenomination.denom_pub];
+ cryptoComp.rsaDenomPubToHash[
+ backupDenomination.denom_pub.rsa_public_key
+ ];
checkLogicInvariant(!!denomPubHash);
const existingDenom = await tx.denominations.get([
backupExchangeDetails.base_url,
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts
index 3f4c02274..9027625cd 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -40,6 +40,7 @@ import {
ConfirmPayResultType,
durationFromSpec,
getTimestampNow,
+ hashDenomPub,
HttpStatusCode,
j2s,
Logger,
@@ -57,10 +58,7 @@ import {
import { gunzipSync, gzipSync } from "fflate";
import { InternalWalletState } from "../../common.js";
import { kdf } from "@gnu-taler/taler-util";
-import {
- secretbox,
- secretbox_open,
-} from "@gnu-taler/taler-util";
+import { secretbox, secretbox_open } from "@gnu-taler/taler-util";
import {
bytesToString,
decodeCrock,
@@ -162,13 +160,16 @@ async function computeBackupCryptoData(
): Promise<BackupCryptoPrecomputedData> {
const cryptoData: BackupCryptoPrecomputedData = {
coinPrivToCompletedCoin: {},
- denomPubToHash: {},
+ rsaDenomPubToHash: {},
proposalIdToContractTermsHash: {},
proposalNoncePrivToPub: {},
reservePrivToPub: {},
};
for (const backupExchangeDetails of backupContent.exchange_details) {
for (const backupDenom of backupExchangeDetails.denominations) {
+ if (backupDenom.denom_pub.cipher !== 1) {
+ throw Error("unsupported cipher");
+ }
for (const backupCoin of backupDenom.coins) {
const coinPub = encodeCrock(
eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
@@ -176,16 +177,16 @@ async function computeBackupCryptoData(
const blindedCoin = rsaBlind(
hash(decodeCrock(backupCoin.coin_priv)),
decodeCrock(backupCoin.blinding_key),
- decodeCrock(backupDenom.denom_pub),
+ decodeCrock(backupDenom.denom_pub.rsa_public_key),
);
cryptoData.coinPrivToCompletedCoin[backupCoin.coin_priv] = {
coinEvHash: encodeCrock(hash(blindedCoin)),
coinPub,
};
}
- cryptoData.denomPubToHash[backupDenom.denom_pub] = encodeCrock(
- hash(decodeCrock(backupDenom.denom_pub)),
- );
+ cryptoData.rsaDenomPubToHash[
+ backupDenom.denom_pub.rsa_public_key
+ ] = encodeCrock(hashDenomPub(backupDenom.denom_pub));
}
for (const backupReserve of backupExchangeDetails.reserves) {
cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 740242050..8fe3702f5 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -25,6 +25,7 @@ import {
ContractTerms,
CreateDepositGroupRequest,
CreateDepositGroupResponse,
+ decodeCrock,
durationFromSpec,
getTimestampNow,
Logger,
@@ -106,7 +107,7 @@ function hashWire(paytoUri: string, salt: string): string {
const r = kdf(
64,
stringToBytes(paytoUri + "\0"),
- stringToBytes(salt + "\0"),
+ decodeCrock(salt),
stringToBytes("merchant-wire-signature"),
);
return encodeCrock(r);
@@ -213,8 +214,8 @@ async function processDepositGroupImpl(
const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
const httpResp = await ws.http.postJson(url.href, {
contribution: Amounts.stringify(perm.contribution),
- wire: depositGroup.wire,
- h_wire: depositGroup.contractTermsRaw.h_wire,
+ merchant_payto_uri: depositGroup.wire.payto_uri,
+ wire_salt: depositGroup.wire.salt,
h_contract_terms: depositGroup.contractTermsHash,
ub_sig: perm.ub_sig,
timestamp: depositGroup.contractTermsRaw.timestamp,
@@ -355,7 +356,7 @@ export async function createDepositGroup(
const timestampRound = timestampTruncateToSecond(timestamp);
const noncePair = await ws.cryptoApi.createEddsaKeypair();
const merchantPair = await ws.cryptoApi.createEddsaKeypair();
- const wireSalt = encodeCrock(getRandomBytes(64));
+ const wireSalt = encodeCrock(getRandomBytes(16));
const wireHash = hashWire(req.depositPaytoUri, wireSalt);
const contractTerms: ContractTerms = {
auditors: [],
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 629957efb..c170c5469 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -39,6 +39,7 @@ import {
URL,
TalerErrorDetails,
Timestamp,
+ hashDenomPub,
} from "@gnu-taler/taler-util";
import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
import { CryptoApi } from "../crypto/workers/cryptoApi.js";
@@ -78,7 +79,7 @@ function denominationRecordFromKeys(
listIssueDate: Timestamp,
denomIn: Denomination,
): DenominationRecord {
- const denomPubHash = encodeCrock(hash(decodeCrock(denomIn.denom_pub)));
+ const denomPubHash = encodeCrock(hashDenomPub(denomIn.denom_pub));
const d: DenominationRecord = {
denomPub: denomIn.denom_pub,
denomPubHash,
@@ -472,26 +473,29 @@ async function updateExchangeFromUrlImpl(
let tosFound: ExchangeTosDownloadResult | undefined;
//Remove this when exchange supports multiple content-type in accept header
- if (acceptedFormat) for (const format of acceptedFormat) {
- const resp = await downloadExchangeWithTermsOfService(
- baseUrl,
- ws.http,
- timeout,
- format
- );
- if (resp.tosContentType === format) {
- tosFound = resp
- break
+ if (acceptedFormat)
+ for (const format of acceptedFormat) {
+ const resp = await downloadExchangeWithTermsOfService(
+ baseUrl,
+ ws.http,
+ timeout,
+ format,
+ );
+ if (resp.tosContentType === format) {
+ tosFound = resp;
+ break;
+ }
}
- }
// If none of the specified format was found try text/plain
- const tosDownload = tosFound !== undefined ? tosFound :
- await downloadExchangeWithTermsOfService(
- baseUrl,
- ws.http,
- timeout,
- "text/plain"
- );
+ const tosDownload =
+ tosFound !== undefined
+ ? tosFound
+ : await downloadExchangeWithTermsOfService(
+ baseUrl,
+ ws.http,
+ timeout,
+ "text/plain",
+ );
let recoupGroupId: string | undefined = undefined;
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index a42480f40..acc592a72 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -175,7 +175,7 @@ export async function getEffectiveDepositAmount(
for (let i = 0; i < pcs.coinPubs.length; i++) {
const coin = await tx.coins.get(pcs.coinPubs[i]);
if (!coin) {
- throw Error("can't calculate deposit amountt, coin not found");
+ throw Error("can't calculate deposit amount, coin not found");
}
const denom = await tx.denominations.get([
coin.exchangeBaseUrl,
@@ -193,6 +193,9 @@ export async function getEffectiveDepositAmount(
if (!exchangeDetails) {
continue;
}
+ // FIXME/NOTE: the line below _likely_ throws exception
+ // about "find method not found on undefined" when the wireType
+ // is not supported by the Exchange.
const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
return timestampIsBetween(
getTimestampNow(),
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index d727bd06f..956e4d65a 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -14,7 +14,12 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { encodeCrock, getRandomBytes, HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ DenomKeyType,
+ encodeCrock,
+ getRandomBytes,
+ HttpStatusCode,
+} from "@gnu-taler/taler-util";
import {
CoinRecord,
CoinSourceType,
@@ -599,10 +604,17 @@ async function refreshReveal(
continue;
}
const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex];
- const denomSig = await ws.cryptoApi.rsaUnblind(
- reveal.ev_sigs[newCoinIndex].ev_sig,
+ if (denom.denomPub.cipher !== 1) {
+ throw Error("cipher unsupported");
+ }
+ const evSig = reveal.ev_sigs[newCoinIndex].ev_sig;
+ if (evSig.cipher !== DenomKeyType.Rsa) {
+ throw Error("unsupported cipher");
+ }
+ const denomSigRsa = await ws.cryptoApi.rsaUnblind(
+ evSig.blinded_rsa_signature,
pc.blindingKey,
- denom.denomPub,
+ denom.denomPub.rsa_public_key,
);
const coin: CoinRecord = {
blindingKey: pc.blindingKey,
@@ -611,7 +623,10 @@ async function refreshReveal(
currentAmount: denom.value,
denomPub: denom.denomPub,
denomPubHash: denom.denomPubHash,
- denomSig,
+ denomSig: {
+ cipher: DenomKeyType.Rsa,
+ rsa_signature: denomSigRsa,
+ },
exchangeBaseUrl: oldCoin.exchangeBaseUrl,
status: CoinStatus.Fresh,
coinSource: {
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
index d2071cd53..d6f0626dd 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -174,7 +174,8 @@ async function registerRandomBankUser(
const reqUrl = new URL("testing/register", bankBaseUrl).href;
const randId = makeId(8);
const bankUser: BankUser = {
- username: `testuser-${randId}`,
+ // euFin doesn't allow resource names to have upper case letters.
+ username: `testuser-${randId.toLowerCase()}`,
password: `testpw-${randId}`,
};
diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts
index a90e5270f..07ce00d2e 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -30,6 +30,7 @@ import {
codecForTipResponse,
Logger,
URL,
+ DenomKeyType,
} from "@gnu-taler/taler-util";
import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
import {
@@ -322,16 +323,20 @@ async function processTipImpl(
const planchet = planchets[i];
checkLogicInvariant(!!planchet);
- const denomSig = await ws.cryptoApi.rsaUnblind(
+ if (denom.denomPub.cipher !== 1) {
+ throw Error("unsupported cipher");
+ }
+
+ const denomSigRsa = await ws.cryptoApi.rsaUnblind(
blindedSig,
planchet.blindingKey,
- denom.denomPub,
+ denom.denomPub.rsa_public_key,
);
const isValid = await ws.cryptoApi.rsaVerify(
planchet.coinPub,
- denomSig,
- denom.denomPub,
+ denomSigRsa,
+ denom.denomPub.rsa_public_key,
);
if (!isValid) {
@@ -364,7 +369,7 @@ async function processTipImpl(
currentAmount: denom.value,
denomPub: denom.denomPub,
denomPubHash: denom.denomPubHash,
- denomSig: denomSig,
+ denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa },
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
status: CoinStatus.Fresh,
suspended: false,
diff --git a/packages/taler-wallet-core/src/operations/withdraw.test.ts b/packages/taler-wallet-core/src/operations/withdraw.test.ts
index b4f0d35e6..179852966 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.test.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.test.ts
@@ -28,8 +28,11 @@ test("withdrawal selection bug repro", (t) => {
const denoms: DenominationRecord[] = [
{
- denomPub:
- "040000XT67C8KBD6B75TTQ3SK8FWXMNQW4372T3BDDGPAMB9RFCA03638W8T3F71WFEFK9NP32VKYVNFXPYRWQ1N1HDKV5J0DFEKHBPJCYSWCBJDRNWD7G8BN8PT97FA9AMV75MYEK4X54D1HGJ207JSVJBGFCATSPNTEYNHEQF1F220W00TBZR1HNPDQFD56FG0DJQ9KGHM8EC33H6AY9YN9CNX5R3Z4TZ4Q23W47SBHB13H6W74FQJG1F50X38VRSC4SR8RWBAFB7S4K8D2H4NMRFSQT892A3T0BTBW7HM5C0H2CK6FRKG31F7W9WP1S29013K5CXYE55CT8TH6N8J9B780R42Y5S3ZB6J6E9H76XBPSGH4TGYSR2VZRB98J417KCQMZKX1BB67E7W5KVE37TC9SJ904002",
+ denomPub: {
+ cipher: 1,
+ rsa_public_key:
+ "040000XT67C8KBD6B75TTQ3SK8FWXMNQW4372T3BDDGPAMB9RFCA03638W8T3F71WFEFK9NP32VKYVNFXPYRWQ1N1HDKV5J0DFEKHBPJCYSWCBJDRNWD7G8BN8PT97FA9AMV75MYEK4X54D1HGJ207JSVJBGFCATSPNTEYNHEQF1F220W00TBZR1HNPDQFD56FG0DJQ9KGHM8EC33H6AY9YN9CNX5R3Z4TZ4Q23W47SBHB13H6W74FQJG1F50X38VRSC4SR8RWBAFB7S4K8D2H4NMRFSQT892A3T0BTBW7HM5C0H2CK6FRKG31F7W9WP1S29013K5CXYE55CT8TH6N8J9B780R42Y5S3ZB6J6E9H76XBPSGH4TGYSR2VZRB98J417KCQMZKX1BB67E7W5KVE37TC9SJ904002",
+ },
denomPubHash:
"Q21FQSSG4FXNT96Z14CHXM8N1RZAG9GPHAV8PRWS0PZAAVWH7PBW6R97M2CH19KKP65NNSWXY7B6S53PT3CBM342E357ZXDDJ8RDVW8",
exchangeBaseUrl: "https://exchange.demo.taler.net/",
@@ -79,8 +82,12 @@ test("withdrawal selection bug repro", (t) => {
listIssueDate: { t_ms: 0 },
},
{
- denomPub:
- "040000Y63CF78QFPKRY77BRK9P557Q1GQWX3NCZ3HSYSK0Z7TT0KGRA7N4SKBKEHSTVHX1Z9DNXMJR4EXSY1TXCKV0GJ3T3YYC6Z0JNMJFVYQAV4FX5J90NZH1N33MZTV8HS9SMNAA9S6K73G4P99GYBB01B0P6M1KXZ5JRDR7VWBR3MEJHHGJ6QBMCJR3NWJRE3WJW9PRY8QPQ2S7KFWTWRESH2DBXCXWBD2SRN6P9YX8GRAEMFEGXC9V5GVJTEMH6ZDGNXFPWZE3JVJ2Q4N9GDYKBCHZCJ7M7M2RJ9ZV4Y64NAN9BT6XDC68215GKKRHTW1BBF1MYY6AR3JCTT9HYAM923RMVQR3TAEB7SDX8J76XRZWYH3AGJCZAQGMN5C8SSH9AHQ9RNQJQ15CN45R37X4YNFJV904002",
+ denomPub: {
+ cipher: 1,
+ rsa_public_key:
+ "040000Y63CF78QFPKRY77BRK9P557Q1GQWX3NCZ3HSYSK0Z7TT0KGRA7N4SKBKEHSTVHX1Z9DNXMJR4EXSY1TXCKV0GJ3T3YYC6Z0JNMJFVYQAV4FX5J90NZH1N33MZTV8HS9SMNAA9S6K73G4P99GYBB01B0P6M1KXZ5JRDR7VWBR3MEJHHGJ6QBMCJR3NWJRE3WJW9PRY8QPQ2S7KFWTWRESH2DBXCXWBD2SRN6P9YX8GRAEMFEGXC9V5GVJTEMH6ZDGNXFPWZE3JVJ2Q4N9GDYKBCHZCJ7M7M2RJ9ZV4Y64NAN9BT6XDC68215GKKRHTW1BBF1MYY6AR3JCTT9HYAM923RMVQR3TAEB7SDX8J76XRZWYH3AGJCZAQGMN5C8SSH9AHQ9RNQJQ15CN45R37X4YNFJV904002",
+ },
+
denomPubHash:
"447WA23SCBATMABHA0793F92MYTBYVPYMMQHCPKMKVY5P7RZRFMQ6VRW0Y8HRA7177GTBT0TBT08R21DZD129AJ995H9G09XBFE55G8",
exchangeBaseUrl: "https://exchange.demo.taler.net/",
@@ -130,8 +137,11 @@ test("withdrawal selection bug repro", (t) => {
listIssueDate: { t_ms: 0 },
},
{
- denomPub:
- "040000YDESWC2B962DA4WK356SC50MA3N9KV0ZSGY3RC48JCTY258W909C7EEMT5BTC5KZ5T4CERCZ141P9QF87EK2BD1XEEM5GB07MB3H19WE4CQGAS8X84JBWN83PQGQXVMWE5HFA992KMGHC566GT9ZS2QPHZB6X89C4A80Z663PYAAPXP728VHAKATGNNBQ01ZZ2XD1CH9Y38YZBSPJ4K7GB2J76GBCYAVD9ENHDVWXJAXYRPBX4KSS5TXRR3K5NEN9ZV3AJD2V65K7ABRZDF5D5V1FJZZMNJ5XZ4FEREEKEBV9TDFPGJTKDEHEC60K3DN24DAATRESDJ1ZYYSYSRCAT4BT2B62ARGVMJTT5N2R126DRW9TGRWCW0ZAF2N2WET1H4NJEW77X0QT46Z5R3MZ0XPHD04002",
+ denomPub: {
+ cipher: 1,
+ rsa_public_key:
+ "040000YDESWC2B962DA4WK356SC50MA3N9KV0ZSGY3RC48JCTY258W909C7EEMT5BTC5KZ5T4CERCZ141P9QF87EK2BD1XEEM5GB07MB3H19WE4CQGAS8X84JBWN83PQGQXVMWE5HFA992KMGHC566GT9ZS2QPHZB6X89C4A80Z663PYAAPXP728VHAKATGNNBQ01ZZ2XD1CH9Y38YZBSPJ4K7GB2J76GBCYAVD9ENHDVWXJAXYRPBX4KSS5TXRR3K5NEN9ZV3AJD2V65K7ABRZDF5D5V1FJZZMNJ5XZ4FEREEKEBV9TDFPGJTKDEHEC60K3DN24DAATRESDJ1ZYYSYSRCAT4BT2B62ARGVMJTT5N2R126DRW9TGRWCW0ZAF2N2WET1H4NJEW77X0QT46Z5R3MZ0XPHD04002",
+ },
denomPubHash:
"JS61DTKAFM0BX8Q4XV3ZSKB921SM8QK745Z2AFXTKFMBHHFNBD8TQ5ETJHFNDGBGX22FFN2A2ERNYG1SGSDQWNQHQQ2B14DBVJYJG8R",
exchangeBaseUrl: "https://exchange.demo.taler.net/",
@@ -181,8 +191,12 @@ test("withdrawal selection bug repro", (t) => {
listIssueDate: { t_ms: 0 },
},
{
- denomPub:
- "040000YG3T1ADB8DVA6BD3EPV6ZHSHTDW35DEN4VH1AE6CSB7P1PSDTNTJG866PHF6QB1CCWYCVRGA0FVBJ9Q0G7KV7AD9010GDYBQH0NNPHW744MTNXVXWBGGGRGQGYK4DTYN1DSWQ1FZNDSZZPB5BEKG2PDJ93NX2JTN06Y8QMS2G734Z9XHC10EENBG2KVB7EJ3CM8PV1T32RC7AY62F3496E8D8KRHJQQTT67DSGMNKK86QXVDTYW677FG27DP20E8XY3M6FQD53NDJ1WWES91401MV1A3VXVPGC76GZVDD62W3WTJ1YMKHTTA3MRXX3VEAAH3XTKDN1ER7X6CZPMYTF8VK735VP2B2TZGTF28TTW4FZS32SBS64APCDF6SZQ427N5538TJC7SRE71YSP5ET8GS904002",
+ denomPub: {
+ cipher: 1,
+ rsa_public_key:
+ "040000YG3T1ADB8DVA6BD3EPV6ZHSHTDW35DEN4VH1AE6CSB7P1PSDTNTJG866PHF6QB1CCWYCVRGA0FVBJ9Q0G7KV7AD9010GDYBQH0NNPHW744MTNXVXWBGGGRGQGYK4DTYN1DSWQ1FZNDSZZPB5BEKG2PDJ93NX2JTN06Y8QMS2G734Z9XHC10EENBG2KVB7EJ3CM8PV1T32RC7AY62F3496E8D8KRHJQQTT67DSGMNKK86QXVDTYW677FG27DP20E8XY3M6FQD53NDJ1WWES91401MV1A3VXVPGC76GZVDD62W3WTJ1YMKHTTA3MRXX3VEAAH3XTKDN1ER7X6CZPMYTF8VK735VP2B2TZGTF28TTW4FZS32SBS64APCDF6SZQ427N5538TJC7SRE71YSP5ET8GS904002",
+ },
+
denomPubHash:
"8T51NEY81VMPQ180EQ5WR0YH7GMNNT90W55Q0514KZM18AZT71FHJGJHQXGK0WTA7ACN1X2SD0S53XPBQ1A9KH960R48VCVVM6E3TH8",
exchangeBaseUrl: "https://exchange.demo.taler.net/",
@@ -232,8 +246,11 @@ test("withdrawal selection bug repro", (t) => {
listIssueDate: { t_ms: 0 },
},
{
- denomPub:
- "040000ZC0G60E9QQ5PD81TSDWD9GV5Y6P8Z05NSPA696DP07NGQQVSRQXBA76Q6PRB0YFX295RG4MTQJXAZZ860ET307HSC2X37XAVGQXRVB8Q4F1V7NP5ZEVKTX75DZK1QRAVHEZGQYKSSH6DBCJNQF6V9WNQF3GEYVA4KCBHA7JF772KHXM9642C28Z0AS4XXXV2PABAN5C8CHYD5H7JDFNK3920W5Q69X0BS84XZ4RE2PW6HM1WZ6KGZ3MKWWWCPKQ1FSFABRBWKAB09PF563BEBXKY6M38QETPH5EDWGANHD0SC3QV0WXYVB7BNHNNQ0J5BNV56K563SYHM4E5ND260YRJSYA1GN5YSW2B1J5T1A1EBNYF2DN6JNJKWXWEQ42G5YS17ZSZ5EWDRA9QKV8EGTCNAD04002",
+ denomPub: {
+ cipher: 1,
+ rsa_public_key:
+ "040000ZC0G60E9QQ5PD81TSDWD9GV5Y6P8Z05NSPA696DP07NGQQVSRQXBA76Q6PRB0YFX295RG4MTQJXAZZ860ET307HSC2X37XAVGQXRVB8Q4F1V7NP5ZEVKTX75DZK1QRAVHEZGQYKSSH6DBCJNQF6V9WNQF3GEYVA4KCBHA7JF772KHXM9642C28Z0AS4XXXV2PABAN5C8CHYD5H7JDFNK3920W5Q69X0BS84XZ4RE2PW6HM1WZ6KGZ3MKWWWCPKQ1FSFABRBWKAB09PF563BEBXKY6M38QETPH5EDWGANHD0SC3QV0WXYVB7BNHNNQ0J5BNV56K563SYHM4E5ND260YRJSYA1GN5YSW2B1J5T1A1EBNYF2DN6JNJKWXWEQ42G5YS17ZSZ5EWDRA9QKV8EGTCNAD04002",
+ },
denomPubHash:
"A41HW0Q2H9PCNMEWW0C0N45QAYVXZ8SBVRRAHE4W6X24SV1TH38ANTWDT80JXEBW9Z8PVPGT9GFV2EYZWJ5JW5W1N34NFNKHQSZ1PFR",
exchangeBaseUrl: "https://exchange.demo.taler.net/",
@@ -283,8 +300,11 @@ test("withdrawal selection bug repro", (t) => {
listIssueDate: { t_ms: 0 },
},
{
- denomPub:
- "040000ZSK2PMVY6E3NBQ52KXMW029M60F4BWYTDS0FZSD0PE53CNZ9H6TM3GQK1WRTEKQ5GRWJ1J9DY6Y42SP47QVT1XD1G0W05SQ5F3F7P5KSWR0FJBJ9NZBXQEVN8Q4JRC94X3JJ3XV3KBYTZ2HTDFV28C3H2SRR0XGNZB4FY85NDZF1G4AEYJJ9QB3C0V8H70YB8RV3FKTNH7XS4K4HFNZHJ5H9VMX5SM9Z2DX37HA5WFH0E2MJBVVF2BWWA5M0HPPSB365RAE2AMD42Q65A96WD80X27SB2ZNQZ8WX0K13FWF85GZ6YNYAJGE1KGN06JDEKE9QD68Z651D7XE8V6664TVVC8M68S7WD0DSXMJQKQ0BNJXNDE29Q7MRX6DA3RW0PZ44B3TKRK0294FPVZTNSTA6XF04002",
+ denomPub: {
+ cipher: 1,
+ rsa_public_key:
+ "040000ZSK2PMVY6E3NBQ52KXMW029M60F4BWYTDS0FZSD0PE53CNZ9H6TM3GQK1WRTEKQ5GRWJ1J9DY6Y42SP47QVT1XD1G0W05SQ5F3F7P5KSWR0FJBJ9NZBXQEVN8Q4JRC94X3JJ3XV3KBYTZ2HTDFV28C3H2SRR0XGNZB4FY85NDZF1G4AEYJJ9QB3C0V8H70YB8RV3FKTNH7XS4K4HFNZHJ5H9VMX5SM9Z2DX37HA5WFH0E2MJBVVF2BWWA5M0HPPSB365RAE2AMD42Q65A96WD80X27SB2ZNQZ8WX0K13FWF85GZ6YNYAJGE1KGN06JDEKE9QD68Z651D7XE8V6664TVVC8M68S7WD0DSXMJQKQ0BNJXNDE29Q7MRX6DA3RW0PZ44B3TKRK0294FPVZTNSTA6XF04002",
+ },
denomPubHash:
"F5NGBX33DTV4595XZZVK0S2MA1VMXFEJQERE5EBP5DS4QQ9EFRANN7YHWC1TKSHT2K6CQWDBRES8D3DWR0KZF5RET40B4AZXZ0RW1ZG",
exchangeBaseUrl: "https://exchange.demo.taler.net/",
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 620ad88be..57bd49d23 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -41,6 +41,7 @@ import {
URL,
WithdrawUriInfoResponse,
VersionMatchResult,
+ DenomKeyType,
} from "@gnu-taler/taler-util";
import {
CoinRecord,
@@ -495,7 +496,7 @@ async function processPlanchetExchangeRequest(
]);
if (!denom) {
- console.error("db inconsistent: denom for planchet not found");
+ logger.error("db inconsistent: denom for planchet not found");
return;
}
@@ -589,16 +590,26 @@ async function processPlanchetVerifyAndStoreCoin(
const { planchet, exchangeBaseUrl } = d;
- const denomSig = await ws.cryptoApi.rsaUnblind(
- resp.ev_sig,
+ const planchetDenomPub = planchet.denomPub;
+ if (planchetDenomPub.cipher !== DenomKeyType.Rsa) {
+ throw Error("cipher not supported");
+ }
+
+ const evSig = resp.ev_sig;
+ if (evSig.cipher !== DenomKeyType.Rsa) {
+ throw Error("unsupported cipher");
+ }
+
+ const denomSigRsa = await ws.cryptoApi.rsaUnblind(
+ evSig.blinded_rsa_signature,
planchet.blindingKey,
- planchet.denomPub,
+ planchetDenomPub.rsa_public_key,
);
const isValid = await ws.cryptoApi.rsaVerify(
planchet.coinPub,
- denomSig,
- planchet.denomPub,
+ denomSigRsa,
+ planchetDenomPub.rsa_public_key,
);
if (!isValid) {
@@ -629,7 +640,10 @@ async function processPlanchetVerifyAndStoreCoin(
currentAmount: planchet.coinValue,
denomPub: planchet.denomPub,
denomPubHash: planchet.denomPubHash,
- denomSig,
+ denomSig: {
+ cipher: DenomKeyType.Rsa,
+ rsa_signature: denomSigRsa,
+ },
coinEvHash: planchet.coinEvHash,
exchangeBaseUrl: exchangeBaseUrl,
status: CoinStatus.Fresh,
@@ -728,7 +742,9 @@ export async function updateWithdrawalDenoms(
batchIdx++, current++
) {
const denom = denominations[current];
- if (denom.verificationStatus === DenominationVerificationStatus.Unverified) {
+ if (
+ denom.verificationStatus === DenominationVerificationStatus.Unverified
+ ) {
logger.trace(
`Validating denomination (${current + 1}/${
denominations.length
@@ -745,7 +761,8 @@ export async function updateWithdrawalDenoms(
);
denom.verificationStatus = DenominationVerificationStatus.VerifiedBad;
} else {
- denom.verificationStatus = DenominationVerificationStatus.VerifiedGood;
+ denom.verificationStatus =
+ DenominationVerificationStatus.VerifiedGood;
}
updatedDenominations.push(denom);
}
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts
index ed48b8dd1..b4dc2a18b 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -33,7 +33,10 @@ function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
return {
availableAmount: a(current),
coinPub: "foobar",
- denomPub: "foobar",
+ denomPub: {
+ cipher: 1,
+ rsa_public_key: "foobar",
+ },
feeDeposit: a(feeDeposit),
exchangeBaseUrl: "https://example.com/",
};
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
index 500cee5d8..ba26c98fe 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -23,7 +23,7 @@
/**
* Imports.
*/
-import { AmountJson, Amounts } from "@gnu-taler/taler-util";
+import { AmountJson, Amounts, DenominationPubKey } from "@gnu-taler/taler-util";
import { strcmp, Logger } from "@gnu-taler/taler-util";
const logger = new Logger("coinSelection.ts");
@@ -72,7 +72,7 @@ export interface AvailableCoinInfo {
/**
* Coin's denomination public key.
*/
- denomPub: string;
+ denomPub: DenominationPubKey;
/**
* Amount still remaining (typically the full amount,
@@ -206,6 +206,21 @@ function tallyFees(
};
}
+function denomPubCmp(
+ p1: DenominationPubKey,
+ p2: DenominationPubKey,
+): -1 | 0 | 1 {
+ if (p1.cipher < p2.cipher) {
+ return -1;
+ } else if (p1.cipher > p2.cipher) {
+ return +1;
+ }
+ if (p1.cipher !== 1 || p2.cipher !== 1) {
+ throw Error("unsupported cipher");
+ }
+ return strcmp(p1.rsa_public_key, p2.rsa_public_key);
+}
+
/**
* Given a list of candidate coins, select coins to spend under the merchant's
* constraints.
@@ -272,7 +287,7 @@ export function selectPayCoins(
(o1, o2) =>
-Amounts.cmp(o1.availableAmount, o2.availableAmount) ||
Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
- strcmp(o1.denomPub, o2.denomPub),
+ denomPubCmp(o1.denomPub, o2.denomPub),
);
// FIXME: Here, we should select coins in a smarter way.
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 32e3945e8..cd2dd7f1e 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -387,6 +387,7 @@ async function runTaskLoop(
} catch (e) {
if (e instanceof OperationFailedAndReportedError) {
logger.warn("operation processed resulted in reported error");
+ logger.warn(`reporred error was: ${j2s(e.operationError)}`);
} else {
logger.error("Uncaught exception", e);
ws.notify({
@@ -929,7 +930,7 @@ async function dispatchRequestInternal(
}
const components = pt.targetPath.split("/");
const creditorAcct = components[components.length - 1];
- logger.info(`making testbank transfer to '${creditorAcct}''`)
+ logger.info(`making testbank transfer to '${creditorAcct}''`);
const fbReq = await ws.http.postJson(
new URL(`${creditorAcct}/admin/add-incoming`, req.bank).href,
{
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
index 3a43f1e76..b3d0b10af 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -13,6 +13,7 @@
"compile": "tsc && rollup -c",
"build-storybook": "build-storybook",
"storybook": "start-storybook -s . -p 6006",
+ "pretty": "prettier --write src",
"watch": "tsc --watch & rollup -w -c"
},
"dependencies": {
@@ -80,4 +81,4 @@
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|po)$": "<rootDir>/tests/__mocks__/fileTransformer.js"
}
}
-}
+} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index 9edd8ca67..56704fb57 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -25,32 +25,32 @@
* Imports.
*/
import { i18n } from "@gnu-taler/taler-util";
-import { ComponentChildren, JSX, h } from "preact";
+import { ComponentChildren, h, VNode } from "preact";
import Match from "preact-router/match";
+import { PopupNavigation } from "./components/styled";
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',
+ 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',
+ reset_required = "/reset-required",
+ payback = "/payback",
+ return_coins = "/return-coins",
- pay = '/pay',
- refund = '/refund',
- tips = '/tip',
- withdraw = '/withdraw',
+ pay = "/pay",
+ refund = "/refund",
+ tips = "/tip",
+ withdraw = "/withdraw",
}
interface TabProps {
@@ -59,7 +59,7 @@ interface TabProps {
children?: ComponentChildren;
}
-function Tab(props: TabProps): JSX.Element {
+function Tab(props: TabProps): VNode {
let cssClass = "";
if (props.current?.startsWith(props.target)) {
cssClass = "active";
@@ -71,23 +71,28 @@ function Tab(props: TabProps): JSX.Element {
);
}
-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>
+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>
+ );
}
export function WalletNavBar() {
- const { devMode } = useDevContext()
- return <Match>{({ path }: any) => {
- console.log("path", path)
- return <NavBar devMode={devMode} path={path} />
- }}</Match>
+ const { devMode } = useDevContext();
+ return (
+ <Match>
+ {({ path }: any) => {
+ console.log("path", path);
+ return <NavBar devMode={devMode} path={path} />;
+ }}
+ </Match>
+ );
}
-
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js
index e9492a2fb..8d958d6bd 100644
--- a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js
+++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js
@@ -21,24 +21,23 @@ exports.BrowserCryptoWorkerFactory = void 0;
* @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;
+ 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
+//# sourceMappingURL=browserCryptoWorkerFactory.js.map
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts
index a8315dc6d..ab20228ef 100644
--- a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts
+++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts
@@ -19,7 +19,10 @@
* @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 {
diff --git a/packages/taler-wallet-webextension/src/browserWorkerEntry.ts b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
index b5c26a7bb..7829e6d65 100644
--- a/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
+++ b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
@@ -42,7 +42,7 @@ async function handleRequest(
}
try {
- const result = (impl as any)[operation](...args);
+ const result = await (impl as any)[operation](...args);
worker.postMessage({ result, id });
} catch (e) {
logger.error("error during operation", e);
diff --git a/packages/taler-wallet-webextension/src/compat.js b/packages/taler-wallet-webextension/src/compat.js
index fdfcbd4b9..48e49a0a7 100644
--- a/packages/taler-wallet-webextension/src/compat.js
+++ b/packages/taler-wallet-webextension/src/compat.js
@@ -21,41 +21,44 @@ exports.getPermissionsApi = exports.isNode = exports.isFirefox = void 0;
* WebExtension APIs consistently.
*/
function isFirefox() {
- const rt = chrome.runtime;
- if (typeof rt.getBrowserInfo === "function") {
- return true;
- }
- return false;
+ 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";
+ 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,
- };
- }
+ 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
+//# sourceMappingURL=compat.js.map
diff --git a/packages/taler-wallet-webextension/src/components/Checkbox.tsx b/packages/taler-wallet-webextension/src/components/Checkbox.tsx
index 2d7b98087..59e84f4b0 100644
--- a/packages/taler-wallet-webextension/src/components/Checkbox.tsx
+++ b/packages/taler-wallet-webextension/src/components/Checkbox.tsx
@@ -14,8 +14,7 @@
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 { h, VNode } from "preact";
interface Props {
enabled: boolean;
@@ -24,7 +23,13 @@ interface Props {
name: string;
description?: string;
}
-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 +37,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..3b9519f39 100644
--- a/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx
+++ b/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx
@@ -14,9 +14,8 @@
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 { h, VNode } from "preact";
interface Props {
enabled: boolean;
@@ -25,28 +24,39 @@ interface Props {
name: string;
}
+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>
+);
-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}>
<span>
- <input type="checkbox" name={name} checked={enabled} disabled={false} />
+ <input
+ type="checkbox"
+ name={name}
+ checked={enabled}
+ disabled={false}
+ />
<div>
<Tick />
</div>
diff --git a/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx b/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx
index f0c682ccb..b57075805 100644
--- a/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx
+++ b/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx
@@ -14,9 +14,15 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- import { JSX, h } from "preact";
+import { h, VNode } from "preact";
-export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean; onToggle: () => void; }): JSX.Element {
+export function DebugCheckbox({
+ enabled,
+ onToggle,
+}: {
+ enabled: boolean;
+ onToggle: () => void;
+}): VNode {
return (
<div>
<input
@@ -24,7 +30,8 @@ export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean; onToggl
onClick={onToggle}
type="checkbox"
id="checkbox-perm"
- style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }} />
+ style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }}
+ />
<label
htmlFor="checkbox-perm"
style={{ marginLeft: "0.5em", fontWeight: "bold" }}
diff --git a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx
index b48deb847..d368a10bf 100644
--- a/packages/taler-wallet-webextension/src/components/Diagnostics.tsx
+++ b/packages/taler-wallet-webextension/src/components/Diagnostics.tsx
@@ -15,58 +15,55 @@
*/
import { WalletDiagnostics } from "@gnu-taler/taler-util";
-import { h } from "preact";
-import { JSX } from "preact/jsx-runtime";
+import { Fragment, h, VNode } from "preact";
import { PageLink } from "../renderHtml";
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 {
if (timedOut) {
return <p>Diagnostics timed out. Could not talk to the wallet backend.</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>
- 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>
- 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>
- );
+ return <Fragment />;
}
+ 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>
+ 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>
+ 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>
+ );
}
return <p>Running diagnostics ...</p>;
diff --git a/packages/taler-wallet-webextension/src/components/EditableText.tsx b/packages/taler-wallet-webextension/src/components/EditableText.tsx
index 6f3388bf9..72bfbe809 100644
--- a/packages/taler-wallet-webextension/src/components/EditableText.tsx
+++ b/packages/taler-wallet-webextension/src/components/EditableText.tsx
@@ -14,9 +14,8 @@
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";
interface Props {
value: string;
@@ -25,25 +24,41 @@ 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 [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)}>edit</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));
+ }}
+ >
+ confirm
+ </button>
+ </div>
+ );
+ };
}
return (
<div>
@@ -54,16 +69,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/ErrorMessage.tsx b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
index cfcef16d5..c6b64fb6a 100644
--- a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
+++ b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
@@ -13,22 +13,35 @@
You should have received a 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 { VNode, h } from "preact";
import { useState } from "preact/hooks";
-import arrowDown from '../../static/img/chevron-down.svg';
+import arrowDown from "../../static/img/chevron-down.svg";
import { ErrorBox } from "./styled";
-export function ErrorMessage({ title, description }: { title?: string|VNode; description?: string; }) {
+export function ErrorMessage({
+ title,
+ description,
+}: {
+ title?: string | VNode;
+ description?: string;
+}) {
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>;
+ 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>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx b/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx
index cfa20280f..a71108c50 100644
--- a/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx
+++ b/packages/taler-wallet-webextension/src/components/ExchangeToS.tsx
@@ -13,66 +13,78 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- import { Fragment, 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 {
+ 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
+ 'ids=terms-of-service'. 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/LogoHeader.tsx b/packages/taler-wallet-webextension/src/components/LogoHeader.tsx
index 9b75c62a1..6c47dc92a 100644
--- a/packages/taler-wallet-webextension/src/components/LogoHeader.tsx
+++ b/packages/taler-wallet-webextension/src/components/LogoHeader.tsx
@@ -17,15 +17,22 @@
import { h } from "preact";
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
+ 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>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/Part.tsx b/packages/taler-wallet-webextension/src/components/Part.tsx
index 75c9df16f..c8ecb46d2 100644
--- a/packages/taler-wallet-webextension/src/components/Part.tsx
+++ b/packages/taler-wallet-webextension/src/components/Part.tsx
@@ -15,18 +15,28 @@
*/
import { AmountLike } from "@gnu-taler/taler-util";
import { ExtraLargeText, LargeText, SmallLightText } from "./styled";
-import { h } from 'preact';
+import { h } from "preact";
-export type Kind = 'positive' | 'negative' | 'neutral';
+export type Kind = "positive" | "negative" | "neutral";
interface Props {
- title: string, text: AmountLike, kind: Kind, big?: boolean
+ title: string;
+ text: AmountLike;
+ kind: Kind;
+ big?: boolean;
}
export function Part({ text, title, kind, big }: Props) {
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>
+ return (
+ <div style={{ margin: "1em" }}>
+ <SmallLightText style={{ margin: ".5em" }}>{title}</SmallLightText>
+ <Text
+ style={{
+ color:
+ kind == "positive" ? "green" : kind == "negative" ? "red" : "black",
+ }}
+ >
+ {text}
+ </Text>
+ </div>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/components/QR.tsx b/packages/taler-wallet-webextension/src/components/QR.tsx
index 8e3f69295..4ff1af961 100644
--- a/packages/taler-wallet-webextension/src/components/QR.tsx
+++ b/packages/taler-wallet-webextension/src/components/QR.tsx
@@ -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, "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>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/components/SelectList.tsx b/packages/taler-wallet-webextension/src/components/SelectList.tsx
index 536e5b89a..78dd2feb4 100644
--- a/packages/taler-wallet-webextension/src/components/SelectList.tsx
+++ b/packages/taler-wallet-webextension/src/components/SelectList.tsx
@@ -14,55 +14,74 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { JSX } from "preact/jsx-runtime";
+import { Fragment, h, VNode } from "preact";
import { NiceSelect } from "./styled/index";
-import { h } from "preact";
interface Props {
value?: string;
onChange: (s: string) => void;
label: string;
list: {
- [label: string]: string
- }
+ [label: string]: string;
+ };
name: string;
description?: string;
canBeNull?: 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,
+ description,
+}: Props): VNode {
+ return (
+ <Fragment>
+ <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>
+ )}
+ </Fragment>
+ );
}
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..452b08334
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Time.tsx
@@ -0,0 +1,41 @@
+/*
+ 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 { Timestamp } from "@gnu-taler/taler-util";
+import { formatISO, format } from "date-fns";
+import { h, VNode } from "preact";
+
+export function Time({
+ timestamp,
+ format: formatString,
+}: {
+ timestamp: Timestamp | 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
index 991e97c94..99ca86385 100644
--- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
+++ b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx
@@ -14,18 +14,33 @@
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 {
+ AmountString,
+ Timestamp,
+ Transaction,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+import { h, VNode } 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';
+import {
+ Column,
+ ExtraLargeText,
+ HistoryRow,
+ SmallLightText,
+ LargeText,
+ LightText,
+} from "./styled/index";
+import { Time } from "./Time";
-export function TransactionItem(props: { tx: Transaction, multiCurrency: boolean }): JSX.Element {
+export function TransactionItem(props: {
+ tx: Transaction;
+ multiCurrency: boolean;
+}): VNode {
const tx = props.tx;
switch (tx.type) {
case TransactionType.Withdrawal:
@@ -110,22 +125,27 @@ export function TransactionItem(props: { tx: Transaction, multiCurrency: boolean
}
}
-function TransactionLayout(props: TransactionLayoutProps): JSX.Element {
- const date = new Date(props.timestamp.t_ms);
- const dateStr = format(date, 'dd MMM, hh:mm')
-
+function TransactionLayout(props: TransactionLayoutProps): VNode {
return (
- <HistoryRow href={Pages.transaction.replace(':tid', props.id)}>
+ <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>}
+ {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>
+ {props.pending && (
+ <LightText style={{ marginTop: 5, marginBottom: 5 }}>
+ Waiting for confirmation
+ </LightText>
+ )}
+ <SmallLightText style={{ marginTop: 5 }}>
+ <Time timestamp={props.timestamp} format="dd MMM, hh:mm" />
+ </SmallLightText>
</Column>
<TransactionAmount
pending={props.pending}
@@ -156,7 +176,7 @@ interface TransactionAmountProps {
multiCurrency: boolean;
}
-function TransactionAmount(props: TransactionAmountProps): JSX.Element {
+function TransactionAmount(props: TransactionAmountProps): VNode {
const [currency, amount] = props.amount.split(":");
let sign: string;
switch (props.debitCreditIndicator) {
@@ -170,14 +190,18 @@ function TransactionAmount(props: TransactionAmountProps): JSX.Element {
sign = "";
}
return (
- <Column style={{
- textAlign: 'center',
- color:
- props.pending ? "gray" :
- (sign === '+' ? 'darkgreen' :
- (sign === '-' ? 'darkred' :
- undefined))
- }}>
+ <Column
+ style={{
+ textAlign: "center",
+ color: props.pending
+ ? "gray"
+ : sign === "+"
+ ? "darkgreen"
+ : sign === "-"
+ ? "darkred"
+ : undefined,
+ }}
+ >
<ExtraLargeText>
{sign}
{amount}
@@ -187,4 +211,3 @@ function TransactionAmount(props: TransactionAmountProps): JSX.Element {
</Column>
);
}
-
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 65c1f49e9..2db7c61f8 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -14,18 +14,17 @@
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';
+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) => p.color};
+`;
export const WalletAction = styled.div`
display: flex;
@@ -36,9 +35,9 @@ export const WalletAction = styled.div`
margin: auto;
height: 100%;
-
+
& h1:first-child {
- margin-top: 0;
+ margin-top: 0;
}
section {
margin-bottom: 2em;
@@ -47,7 +46,7 @@ export const WalletAction = styled.div`
margin-left: 8px;
}
}
-`
+`;
export const WalletActionOld = styled.section`
border: solid 5px black;
border-radius: 10px;
@@ -59,17 +58,17 @@ export const WalletActionOld = styled.section`
margin: auto;
height: 100%;
-
+
& h1:first-child {
- margin-top: 0;
+ margin-top: 0;
}
-`
+`;
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;
@@ -79,14 +78,14 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
width: 400px;
}
& > section {
- padding-left: ${({ noPadding }) => noPadding ? '0px' : '8px'};
- padding-right: ${({ noPadding }) => noPadding ? '0px' : '8px'};
+ 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;
+ margin-bottom: auto;
overflow: auto;
table td {
- padding: 5px 10px;
+ padding: 5px 5px;
}
table tr {
border-bottom: 1px solid black;
@@ -128,13 +127,13 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
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;
@@ -144,9 +143,9 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`
justify-content: space-between;
& > section {
- padding: ${({ noPadding }) => noPadding ? '0px' : '8px'};
+ padding: ${({ noPadding }) => (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 {
@@ -201,8 +200,7 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>`
margin-left: 8px;
}
}
-
-`
+`;
export const Button = styled.button<{ upperCased?: boolean }>`
display: inline-block;
@@ -214,7 +212,7 @@ export const Button = styled.button<{ upperCased?: boolean }>`
cursor: pointer;
user-select: none;
box-sizing: border-box;
- text-transform: ${({ upperCased }) => upperCased ? 'uppercase' : 'none'};
+ text-transform: ${({ upperCased }) => (upperCased ? "uppercase" : "none")};
font-family: inherit;
font-size: 100%;
@@ -223,7 +221,7 @@ export const Button = styled.button<{ upperCased?: boolean }>`
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';
+ background-color: "#e6e6e6";
text-decoration: none;
border-radius: 2px;
@@ -263,7 +261,7 @@ export const Link = styled.a<{ upperCased?: boolean }>`
cursor: pointer;
user-select: none;
box-sizing: border-box;
- text-transform: ${({ upperCased }) => upperCased ? 'uppercase' : 'none'};
+ text-transform: ${({ upperCased }) => (upperCased ? "uppercase" : "none")};
font-family: inherit;
font-size: 100%;
@@ -304,9 +302,9 @@ export const FontIcon = styled.div`
text-align: center;
font-weight: bold;
/* vertical-align: text-top; */
-`
+`;
export const ButtonBox = styled(Button)`
- padding: .5em;
+ padding: 0.5em;
width: fit-content;
height: 2em;
@@ -322,89 +320,88 @@ export const ButtonBox = styled(Button)`
border-radius: 4px;
border-color: black;
color: black;
-`
-
+`;
const ButtonVariant = styled(Button)`
color: white;
border-radius: 4px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
-`
+`;
-export const ButtonPrimary = styled(ButtonVariant)`
+export const ButtonPrimary = styled(ButtonVariant)<{ small?: boolean }>`
+ font-size: ${({ small }) => (small ? "small" : "inherit")};
background-color: rgb(66, 184, 221);
-`
+`;
export const ButtonBoxPrimary = styled(ButtonBox)`
color: rgb(66, 184, 221);
border-color: rgb(66, 184, 221);
-`
+`;
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 +411,7 @@ export const RowLightBorderGray = styled(Row2)`
border: 1px solid lightgray;
background-color: red;
}
-`
+`;
export const HistoryRow = styled.a`
text-decoration: none;
@@ -423,7 +420,7 @@ export const HistoryRow = styled.a`
display: flex;
justify-content: space-between;
padding: 0.5em;
-
+
border: 1px solid lightgray;
border-top: 0px;
@@ -439,7 +436,7 @@ export const HistoryRow = styled.a`
margin-left: auto;
align-self: center;
}
-`
+`;
export const ListOfProducts = styled.div`
& > div > a > img {
@@ -453,83 +450,94 @@ export const ListOfProducts = styled.div`
margin-right: auto;
margin-left: 1em;
}
-`
+`;
export const LightText = styled.div`
color: gray;
-`
+`;
export const WarningText = styled.div`
color: rgb(223, 117, 20);
-`
+`;
export const SmallText = styled.div`
- font-size: small;
-`
+ font-size: small;
+`;
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 }) => (!invalid ? "inherit" : "red")};
}
& input {
display: block;
padding: 5px;
width: calc(100% - 4px - 10px);
- border-color: ${({ invalid }) => !invalid ? 'inherit' : 'red'}
+ border-color: ${({ invalid }) => (!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 }) => (!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 }) => (!invalid ? "lightgray" : "red")};
+ }
+`;
export const ErrorBox = styled.div`
border: 2px solid #f5c6cb;
@@ -539,6 +547,7 @@ export const ErrorBox = styled.div`
flex-direction: column;
/* margin: 0.5em; */
padding: 1em;
+ margin: 1em;
/* width: 100%; */
color: #721c24;
background: #f8d7da;
@@ -555,22 +564,22 @@ export const ErrorBox = styled.div`
width: 28px;
}
}
-`
+`;
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 PopupNavigation = styled.div<{ devMode?: boolean }>`
- background-color:#0042b2;
+ background-color: #0042b2;
height: 35px;
justify-content: space-around;
display: flex;
@@ -582,7 +591,7 @@ export const PopupNavigation = styled.div<{ devMode?: boolean }>`
& > div > a {
color: #f8faf7;
display: inline-block;
- width: calc(400px / ${({ devMode }) => !devMode ? 4 : 5});
+ width: calc(400px / ${({ devMode }) => (!devMode ? 4 : 5)});
text-align: center;
text-decoration: none;
vertical-align: middle;
@@ -596,8 +605,9 @@ export const PopupNavigation = styled.div<{ devMode?: boolean }>`
}
`;
-export const NiceSelect = styled.div`
+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 +615,18 @@ export const NiceSelect = styled.div`
appearance: none;
outline: 0;
box-shadow: none;
- background-image: none;
+
+ background-image: ${image};
+ background-position: right 0.5rem center;
+ background-repeat: no-repeat;
+ background-size: 1.5em 1.5em;
+ padding-right: 2.5rem;
+
background-color: white;
- flex: 1;
- padding: 0.5em 1em;
+ border-radius: 0.25rem;
+ font-size: 1em;
+ padding: 0.5em 3em 0.5em 1em;
cursor: pointer;
}
@@ -617,29 +634,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 +643,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 +659,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,11 +676,11 @@ 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";
}
`;
@@ -712,13 +707,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,22 +724,20 @@ 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;
span {
-
input {
display: none;
opacity: 0;
@@ -758,7 +751,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);
@@ -776,12 +769,11 @@ 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
+`;
diff --git a/packages/taler-wallet-webextension/src/context/devContext.ts b/packages/taler-wallet-webextension/src/context/devContext.ts
index ea2ba4ceb..0344df057 100644
--- a/packages/taler-wallet-webextension/src/context/devContext.ts
+++ b/packages/taler-wallet-webextension/src/context/devContext.ts
@@ -15,13 +15,13 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createContext, h, VNode } from 'preact'
-import { useContext, useState } from 'preact/hooks'
-import { useLocalStorage } from '../hooks/useLocalStorage';
+import { createContext, h, VNode } from "preact";
+import { useContext, useState } from "preact/hooks";
+import { useLocalStorage } from "../hooks/useLocalStorage";
interface Type {
devMode: boolean;
@@ -29,14 +29,14 @@ interface Type {
}
const Context = createContext<Type>({
devMode: false,
- toggleDevMode: () => null
-})
+ 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)
+ 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/translation.ts b/packages/taler-wallet-webextension/src/context/translation.ts
index 5f57958de..105da9dcf 100644
--- a/packages/taler-wallet-webextension/src/context/translation.ts
+++ b/packages/taler-wallet-webextension/src/context/translation.ts
@@ -15,54 +15,58 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createContext, h, VNode } from 'preact'
-import { useContext, useEffect } from 'preact/hooks'
-import { useLang } from '../hooks/useLang'
+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';
+import { setupI18n } from "@gnu-taler/taler-util";
interface Type {
lang: string;
changeLanguage: (l: string) => void;
}
const initial = {
- lang: 'en',
+ lang: "en",
changeLanguage: () => {
// do not change anything
- }
-}
-const Context = createContext<Type>(initial)
+ },
+};
+const Context = createContext<Type>(initial);
interface Props {
- initial?: string,
- children: any,
- forceLang?: string
+ 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)
+//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)
+ changeLanguage(forceLang);
}
- })
- useEffect(()=> {
- setupI18n(lang, strings)
- },[lang])
+ });
+ useEffect(() => {
+ setupI18n(lang, strings);
+ }, [lang]);
if (forceLang) {
- setupI18n(forceLang, strings)
+ setupI18n(forceLang, strings);
} else {
- setupI18n(lang, strings)
+ 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/Pay.stories.tsx b/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx
index 622e7950f..c2d360d3b 100644
--- a/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Pay.stories.tsx
@@ -15,150 +15,156 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { ContractTerms, PreparePayResultType } from '@gnu-taler/taler-util';
-import { createExample } from '../test-utils';
-import { PaymentRequestView as TestedComponent } from './Pay';
+import { ContractTerms, PreparePayResultType } from "@gnu-taler/taler-util";
+import { createExample } from "../test-utils";
+import { PaymentRequestView as TestedComponent } from "./Pay";
export default {
- title: 'cta/pay',
+ title: "cta/pay",
component: TestedComponent,
- argTypes: {
- },
+ argTypes: {},
};
export const NoBalance = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.InsufficientBalance,
- noncePriv: '',
+ noncePriv: "",
proposalId: "proposal1234",
- contractTerms: {
+ contractTerms: ({
merchant: {
- name: 'someone'
+ name: "someone",
},
- summary: 'some beers',
- amount: 'USD:10',
- } as Partial<ContractTerms> as any,
- amountRaw: 'USD:10',
- }
+ summary: "some beers",
+ amount: "USD:10",
+ } as Partial<ContractTerms>) as any,
+ amountRaw: "USD:10",
+ },
});
export const NoEnoughBalance = createExample(TestedComponent, {
payStatus: {
status: PreparePayResultType.InsufficientBalance,
- noncePriv: '',
+ noncePriv: "",
proposalId: "proposal1234",
- contractTerms: {
+ contractTerms: ({
merchant: {
- name: 'someone'
+ name: "someone",
},
- summary: 'some beers',
- amount: 'USD:10',
- } as Partial<ContractTerms> as any,
- amountRaw: 'USD:10',
+ summary: "some beers",
+ amount: "USD:10",
+ } as Partial<ContractTerms>) as any,
+ amountRaw: "USD:10",
},
balance: {
- currency: 'USD',
+ currency: "USD",
fraction: 40000000,
- value: 9
- }
+ value: 9,
+ },
});
export const PaymentPossible = createExample(TestedComponent, {
- uri: 'taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0',
+ 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',
+ amountEffective: "USD:10",
+ amountRaw: "USD:10",
+ noncePriv: "",
+ contractTerms: ({
+ nonce: "123213123",
merchant: {
- name: 'someone'
+ name: "someone",
},
- amount: 'USD:10',
- summary: 'some beers',
- } as Partial<ContractTerms> as any,
- contractTermsHash: '123456',
- proposalId: 'proposal1234'
- }
+ 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',
+ 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',
+ amountEffective: "USD:10.20",
+ amountRaw: "USD:10",
+ noncePriv: "",
+ contractTerms: ({
+ nonce: "123213123",
merchant: {
- name: 'someone'
+ name: "someone",
},
- amount: 'USD:10',
- summary: 'some beers',
- } as Partial<ContractTerms> as any,
- contractTermsHash: '123456',
- proposalId: 'proposal1234'
- }
+ 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: {
+ amountEffective: "USD:10",
+ amountRaw: "USD:10",
+ contractTerms: ({
merchant: {
- name: 'someone'
+ 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',
+ 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 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: {
+ amountEffective: "USD:10",
+ amountRaw: "USD:10",
+ contractTerms: ({
merchant: {
- name: 'someone'
+ 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',
+ 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
index 675b14ff9..d5861c47c 100644
--- a/packages/taler-wallet-webextension/src/cta/Pay.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx
@@ -24,18 +24,36 @@
*/
// 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 {
+ AmountJson,
+ AmountLike,
+ Amounts,
+ ConfirmPayResult,
+ ConfirmPayResultDone,
+ ConfirmPayResultType,
+ ContractTerms,
+ i18n,
+ PreparePayResult,
+ PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, 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 {
+ ButtonSuccess,
+ ErrorBox,
+ LinkSuccess,
+ SuccessBox,
+ WalletAction,
+ WarningBox,
+} from "../components/styled";
import { useBalances } from "../hooks/useBalances";
import * as wxApi from "../wxApi";
interface Props {
- talerPayUri?: string
+ talerPayUri?: string;
}
// export function AlreadyPaid({ payStatus }: { payStatus: PreparePayResult }) {
@@ -64,7 +82,9 @@ interface Props {
// </section>
// }
-const doPayment = async (payStatus: PreparePayResult): Promise<ConfirmPayResultDone> => {
+const doPayment = async (
+ payStatus: PreparePayResult,
+): Promise<ConfirmPayResultDone> => {
if (payStatus.status !== "payment-possible") {
throw Error(`invalid state: ${payStatus.status}`);
}
@@ -80,18 +100,29 @@ const doPayment = async (payStatus: PreparePayResult): Promise<ConfirmPayResultD
return res;
};
-
-
-export function PayPage({ talerPayUri }: Props): JSX.Element {
- const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(undefined);
- const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(undefined);
+export function PayPage({ talerPayUri }: Props): VNode {
+ 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 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
+ 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;
@@ -101,7 +132,7 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {
setPayStatus(p);
} catch (e) {
if (e instanceof Error) {
- setPayErrMsg(e.message)
+ setPayErrMsg(e.message);
}
}
};
@@ -109,30 +140,28 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {
}, [talerPayUri]);
if (!talerPayUri) {
- return <span>missing pay uri</span>
+ 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 (
+ <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 () => {
+ const onClick = async (): Promise<void> => {
try {
- const res = await doPayment(payStatus)
+ const res = await doPayment(payStatus);
setPayResult(res);
} catch (e) {
console.error(e);
@@ -140,13 +169,18 @@ export function PayPage({ talerPayUri }: Props): JSX.Element {
setPayErrMsg(e.message);
}
}
+ };
- }
-
- return <PaymentRequestView uri={talerPayUri}
- payStatus={payStatus} payResult={payResult}
- onClick={onClick} payErrMsg={payErrMsg}
- balance={foundAmount} />;
+ return (
+ <PaymentRequestView
+ uri={talerPayUri}
+ payStatus={payStatus}
+ payResult={payResult}
+ onClick={onClick}
+ payErrMsg={payErrMsg}
+ balance={foundAmount}
+ />
+ );
}
export interface PaymentRequestViewProps {
@@ -157,7 +191,14 @@ export interface PaymentRequestViewProps {
uri: string;
balance: AmountJson | undefined;
}
-export function PaymentRequestView({ uri, payStatus, payResult, onClick, payErrMsg, balance }: PaymentRequestViewProps) {
+export function PaymentRequestView({
+ uri,
+ payStatus,
+ payResult,
+ onClick,
+ payErrMsg,
+ balance,
+}: PaymentRequestViewProps): VNode {
let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw);
const contractTerms: ContractTerms = payStatus.contractTerms;
@@ -184,117 +225,175 @@ export function PaymentRequestView({ uri, payStatus, payResult, onClick, payErrM
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 Alternative(): VNode {
+ 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() {
+ function ButtonsSection(): VNode {
if (payResult) {
if (payResult.type === ConfirmPayResultType.Pending) {
- return <section>
- <div>
- <p>Processing...</p>
- </div>
- </section>
+ return (
+ <section>
+ <div>
+ <p>Processing...</p>
+ </div>
+ </section>
+ );
}
- return null
+ return <Fragment />;
}
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>
+ return (
<section>
- <ButtonSuccess upperCased onClick={onClick}>
- {i18n.str`Pay`} {amountToString(payStatus.amountEffective)}
- </ButtonSuccess>
+ <div>
+ <p>Payment failed: {payErrMsg}</p>
+ <button class="pure-button button-success" onClick={onClick}>
+ {i18n.str`Retry`}
+ </button>
+ </div>
</section>
- <Alternative />
- </Fragment>
+ );
+ }
+ 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>
+ 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 (
+ <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 <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 />
+ return (
+ <WalletAction>
+ <LogoHeader />
- </WalletAction>
+ <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}`
+function amountToString(text: AmountLike): string {
+ const aj = Amounts.jsonifyAmount(text);
+ const amount = Amounts.stringifyValue(aj, 2);
+ return `${amount} ${aj.currency}`;
}
diff --git a/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
index 88e714cb7..a0abcea58 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund.stories.tsx
@@ -15,63 +15,61 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { OrderShortInfo } from '@gnu-taler/taler-util';
-import { createExample } from '../test-utils';
-import { View as TestedComponent } from './Refund';
+ *
+ * @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',
+ title: "cta/refund",
component: TestedComponent,
- argTypes: {
- },
+ 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,
+ 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,
+ 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,
+ 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
index 943095360..cecd1ac00 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund.tsx
@@ -20,47 +20,47 @@
* @author Florian Dold
*/
-import * as wxApi from "../wxApi";
-import { AmountView } from "../renderHtml";
-import {
- ApplyRefundResponse,
- Amounts,
-} from "@gnu-taler/taler-util";
+import { Amounts, ApplyRefundResponse } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
-import { JSX } from "preact/jsx-runtime";
-import { h } from 'preact';
+import { AmountView } from "../renderHtml";
+import * as wxApi from "../wxApi";
interface Props {
- talerRefundUri?: string
+ 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) ? (
+export function View({ applyResult }: ViewProps): VNode {
+ return (
+ <section class="main">
+ <h1>GNU Taler Wallet</h1>
+ <article class="fade">
+ <h2>Refund Status</h2>
<p>
- The refund amount of{" "}
- <AmountView amount={applyResult.amountRefundGone} />{" "}
- could not be applied.
+ The product <em>{applyResult.info.summary}</em> has received a total
+ effective refund of{" "}
+ <AmountView amount={applyResult.amountRefundGranted} />.
</p>
- ) : null}
- </article>
- </section>
+ {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);
+export function RefundPage({ talerRefundUri }: Props): VNode {
+ const [applyResult, setApplyResult] = useState<
+ ApplyRefundResponse | undefined
+ >(undefined);
const [errMsg, setErrMsg] = useState<string | undefined>(undefined);
useEffect(() => {
@@ -70,9 +70,10 @@ export function RefundPage({ talerRefundUri }: Props): JSX.Element {
const result = await wxApi.applyRefund(talerRefundUri);
setApplyResult(result);
} catch (e) {
- console.error(e);
- setErrMsg(e.message);
- console.log("err message", e.message);
+ if (e instanceof Error) {
+ setErrMsg(e.message);
+ console.log("err message", e.message);
+ }
}
};
doFetch();
diff --git a/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx b/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx
index 389b183f0..8da599513 100644
--- a/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Tip.stories.tsx
@@ -15,45 +15,43 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample } from '../test-utils';
-import { View as TestedComponent } from './Tip';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample } from "../test-utils";
+import { View as TestedComponent } from "./Tip";
export default {
- title: 'cta/tip',
+ title: "cta/tip",
component: TestedComponent,
- argTypes: {
- },
+ argTypes: {},
};
export const Accepted = createExample(TestedComponent, {
prepareTipResult: {
accepted: true,
- merchantBaseUrl: '',
- exchangeBaseUrl: '',
- expirationTimestamp : {
- t_ms: 0
+ merchantBaseUrl: "",
+ exchangeBaseUrl: "",
+ expirationTimestamp: {
+ t_ms: 0,
},
- tipAmountEffective: 'USD:10',
- tipAmountRaw: 'USD:5',
- walletTipId: 'id'
- }
+ 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
+ merchantBaseUrl: "http://merchant.url/",
+ exchangeBaseUrl: "http://exchange.url/",
+ expirationTimestamp: {
+ t_ms: 0,
},
- tipAmountEffective: 'USD:10',
- tipAmountRaw: 'USD:5',
- walletTipId: 'id'
- }
+ 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
index dc1feaed3..5a9ab720d 100644
--- a/packages/taler-wallet-webextension/src/cta/Tip.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Tip.tsx
@@ -20,51 +20,54 @@
* @author Florian Dold <dold@taler.net>
*/
-import { useEffect, useState } from "preact/hooks";
import { PrepareTipResult } from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
import { AmountView } from "../renderHtml";
import * as wxApi from "../wxApi";
-import { JSX } from "preact/jsx-runtime";
-import { h } from 'preact';
interface Props {
- talerTipUri?: string
+ 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>
- ) : (
+export function View({
+ prepareTipResult,
+ onAccept,
+ onIgnore,
+}: ViewProps): VNode {
+ 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{" "}
+ offering you a tip of{" "}
<strong>
<AmountView amount={prepareTipResult.tipAmountEffective} />
</strong>{" "}
- via the exchange <code>{prepareTipResult.exchangeBaseUrl}</code>
+ via the exchange <code>{prepareTipResult.exchangeBaseUrl}</code>
</p>
<button onClick={onAccept}>Accept tip</button>
<button onClick={onIgnore}>Ignore</button>
</div>
)}
- </article>
- </section>
-
+ </article>
+ </section>
+ );
}
-export function TipPage({ talerTipUri }: Props): JSX.Element {
+export function TipPage({ talerTipUri }: Props): VNode {
const [updateCounter, setUpdateCounter] = useState<number>(0);
const [prepareTipResult, setPrepareTipResult] = useState<
PrepareTipResult | undefined
@@ -105,7 +108,11 @@ export function TipPage({ talerTipUri }: Props): JSX.Element {
return <span>Loading ...</span>;
}
- return <View prepareTipResult={prepareTipResult}
- onAccept={doAccept} onIgnore={doIgnore}
- />
+ return (
+ <View
+ prepareTipResult={prepareTipResult}
+ onAccept={doAccept}
+ onIgnore={doIgnore}
+ />
+ );
}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
index 5e29a3e39..54ae19c61 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
@@ -15,23 +15,19 @@
*/
/**
-*
-* @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';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { amountFractionalBase } from "@gnu-taler/taler-util";
+import { createExample } from "../test-utils";
+import { View as TestedComponent } from "./Withdraw";
export default {
- title: 'cta/withdraw',
+ title: "cta/withdraw",
component: TestedComponent,
argTypes: {
- onSwitchExchange: { action: 'onRetry' },
+ onSwitchExchange: { action: "onRetry" },
},
};
@@ -48,7 +44,7 @@ const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
</div>
</body>
</html>
-`
+`;
const termsPlain = `
Terms Of Service
****************
@@ -432,7 +428,7 @@ 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"?>
<!DOCTYPE document PUBLIC "+//IDN docutils.sourceforge.net//DTD Docutils Generic//EN//XML" "http://docutils.sourceforge.net/docs/ref/docutils.dtd">
@@ -781,120 +777,119 @@ const termsXml = `<?xml version="1.0" encoding="utf-8"?>
`;
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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: 0,
- value: 0
+ value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
},
- onSwitchExchange: async () => { },
+ onSwitchExchange: async () => {
+ null;
+ },
terms: {
value: {
- type: 'xml',
+ type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
- status: 'new'
+ 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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: 0,
- value: 0
+ value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
},
- onSwitchExchange: async () => { },
+ onSwitchExchange: async () => {
+ null;
+ },
terms: {
value: {
- type: 'plain',
- content: termsPlain
+ type: "plain",
+ content: termsPlain,
},
- status: 'new'
+ status: "new",
},
- reviewing: true
-})
+ 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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: 0,
- value: 0
+ value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
},
- onSwitchExchange: async () => { },
+ onSwitchExchange: async () => {
+ null;
+ },
terms: {
value: {
- type: 'html',
- href: new URL(`data:text/html;base64,${Buffer.from(termsHtml).toString('base64')}`),
+ type: "html",
+ href: new URL(
+ `data:text/html;base64,${Buffer.from(termsHtml).toString("base64")}`,
+ ),
},
- status: 'new'
+ status: "new",
},
- reviewing: true
-})
+ reviewing: true,
+});
const termsPdf = `
%PDF-1.2
@@ -909,306 +904,298 @@ 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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: 0,
- value: 0
+ value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
},
- onSwitchExchange: async () => { },
+ onSwitchExchange: async () => {
+ null;
+ },
terms: {
value: {
- type: 'pdf',
- location: new URL(`data:text/html;base64,${Buffer.from(termsPdf).toString('base64')}`),
+ type: "pdf",
+ location: new URL(
+ `data:text/html;base64,${Buffer.from(termsPdf).toString("base64")}`,
+ ),
},
- status: 'new'
+ status: "new",
},
- reviewing: true
-})
-
+ 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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: 0,
- value: 0
+ value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
},
- onSwitchExchange: async () => { },
+ onSwitchExchange: async () => {
+ null;
+ },
terms: {
value: {
- type: 'xml',
+ type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
- status: 'new'
+ status: "new",
},
- reviewing: true
-})
+ 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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: 0,
- value: 0
+ value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
+ },
+ onSwitchExchange: async () => {
+ null;
},
- onSwitchExchange: async () => { },
terms: {
value: {
- type: 'xml',
+ type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
- status: 'new'
+ status: "new",
},
- reviewed: true
-})
+ 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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: 0,
- value: 0
+ value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
},
- onSwitchExchange: async () => { },
+ onSwitchExchange: async () => {
+ null;
+ },
terms: {
value: {
- type: 'xml',
+ type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
- status: 'new'
+ 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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: 0,
- value: 0
+ value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
},
- onSwitchExchange: async () => { },
+ onSwitchExchange: async () => {
+ null;
+ },
terms: {
value: {
- type: 'xml',
+ type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
- status: 'changed'
+ 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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: 0,
- value: 0
+ value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
},
- onSwitchExchange: async () => { },
+ onSwitchExchange: async () => {
+ null;
+ },
terms: {
- status: 'notfound'
+ 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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: amountFractionalBase * 0.5,
- value: 0
+ value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
},
- onSwitchExchange: async () => { },
+ onSwitchExchange: async () => {
+ null;
+ },
terms: {
- status: 'accepted'
+ 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,
- },
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.demo.taler.net",
+ paytoUris: ["asd"],
+ },
+ {
+ currency: "USD",
+ exchangeBaseUrl: "exchange.test.taler.net",
+ paytoUris: ["asd"],
+ },
+ ],
+ exchangeBaseUrl: "exchange.demo.taler.net",
withdrawalFee: {
- currency: 'USD',
+ currency: "USD",
fraction: 0,
value: 0,
},
amount: {
- currency: 'USD',
+ currency: "USD",
value: 2,
- fraction: 10000000
+ fraction: 10000000,
},
- onSwitchExchange: async () => { },
+ onSwitchExchange: async () => {
+ null;
+ },
terms: {
value: {
- type: 'xml',
+ type: "xml",
document: new DOMParser().parseFromString(termsXml, "text/xml"),
},
- status: 'accepted',
- }
-}) \ No newline at end of file
+ status: "accepted",
+ },
+});
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
index 6ef72cbe6..8258717bd 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
@@ -21,28 +21,44 @@
* @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 {
+ AmountJson,
+ Amounts,
+ ExchangeListItem,
+ GetExchangeTosResult,
+ i18n,
+ WithdrawUriInfoResponse,
+} from "@gnu-taler/taler-util";
+import { VNode, h, Fragment } from "preact";
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 { 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,
+ TermsOfService,
+ WalletAction,
+ WarningText,
+} from "../components/styled";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
import {
- acceptWithdrawal, getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, setExchangeTosAccepted, listExchanges, getExchangeTos
+ acceptWithdrawal,
+ getExchangeTos,
+ getExchangeWithdrawalInfo,
+ getWithdrawalDetailsForUri,
+ listExchanges,
+ setExchangeTosAccepted,
} from "../wxApi";
-import { wxMain } from '../wxBackend.js';
interface Props {
talerWithdrawUri?: string;
}
export interface ViewProps {
- details: GetExchangeTosResult;
withdrawalFee: AmountJson;
exchangeBaseUrl: string;
amount: AmountJson;
@@ -58,145 +74,192 @@ export interface ViewProps {
status: TermsStatus;
};
knownExchanges: ExchangeListItem[];
+}
-};
-
-type TermsStatus = 'new' | 'accepted' | 'changed' | 'notfound';
+type TermsStatus = "new" | "accepted" | "changed" | "notfound";
-type TermsDocument = TermsDocumentXml | TermsDocumentHtml | TermsDocumentPlain | TermsDocumentJson | TermsDocumentPdf;
+type TermsDocument =
+ | TermsDocumentXml
+ | TermsDocumentHtml
+ | TermsDocumentPlain
+ | TermsDocumentJson
+ | TermsDocumentPdf;
interface TermsDocumentXml {
- type: 'xml',
- document: Document,
+ type: "xml";
+ document: Document;
}
interface TermsDocumentHtml {
- type: 'html',
- href: URL,
+ type: "html";
+ href: URL;
}
interface TermsDocumentPlain {
- type: 'plain',
- content: string,
+ type: "plain";
+ content: string;
}
interface TermsDocumentJson {
- type: 'json',
- data: any,
+ type: "json";
+ data: any;
}
interface TermsDocumentPdf {
- type: 'pdf',
- location: URL,
+ type: "pdf";
+ location: URL;
}
-function amountToString(text: AmountJson) {
- const aj = Amounts.jsonifyAmount(text)
- const amount = Amounts.stringifyValue(aj)
- return `${amount} ${aj.currency}`
+function amountToString(text: AmountJson): string {
+ 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 }), {})
+export function View({
+ withdrawalFee,
+ exchangeBaseUrl,
+ knownExchanges,
+ amount,
+ onWithdraw,
+ onSwitchExchange,
+ terms,
+ reviewing,
+ onReview,
+ onAccept,
+ reviewed,
+ confirmed,
+}: ViewProps): VNode {
+ 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>
+ <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 />
+ <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 &&
+ {!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("")}>
+ {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>}
-
+ </LinkSuccess>
+ )}
</section>
- }
- {!reviewing && reviewed &&
+ )}
+ {!reviewing && reviewed && (
<section>
- <LinkSuccess
- upperCased
- onClick={() => onReview(true)}
- >
+ <LinkSuccess upperCased onClick={() => onReview(true)}>
{i18n.str`Show terms of service`}
</LinkSuccess>
</section>
- }
- {terms.status === 'notfound' &&
+ )}
+ {terms.status === "notfound" && (
<section>
<WarningText>
{i18n.str`Exchange doesn't have terms of service`}
</WarningText>
</section>
- }
- {reviewing &&
+ )}
+ {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 &&
+ {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)}
- >
+ <LinkSuccess upperCased onClick={() => onReview(false)}>
{i18n.str`Hide terms of service`}
</LinkSuccess>
</section>
- }
- {(reviewing || reviewed) &&
+ )}
+ {(reviewing || reviewed) && (
<section>
<CheckboxOutlined
name="terms"
enabled={reviewed}
label={i18n.str`I accept the exchange terms of service`}
onToggle={() => {
- onAccept(!reviewed)
- onReview(false)
+ onAccept(!reviewed);
+ onReview(false);
}}
/>
</section>
- }
+ )}
{/**
* Main action section
*/}
<section>
- {terms.status === 'new' && !reviewed && !reviewing &&
+ {terms.status === "new" && !reviewed && !reviewing && (
<ButtonSuccess
upperCased
disabled={!exchangeBaseUrl}
@@ -204,8 +267,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,
>
{i18n.str`Review exchange terms of service`}
</ButtonSuccess>
- }
- {terms.status === 'changed' && !reviewed && !reviewing &&
+ )}
+ {terms.status === "changed" && !reviewed && !reviewing && (
<ButtonWarning
upperCased
disabled={!exchangeBaseUrl}
@@ -213,8 +276,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,
>
{i18n.str`Review new version of terms of service`}
</ButtonWarning>
- }
- {(terms.status === 'accepted' || (needsReview && reviewed)) &&
+ )}
+ {(terms.status === "accepted" || (needsReview && reviewed)) && (
<ButtonSuccess
upperCased
disabled={!exchangeBaseUrl || confirmed}
@@ -222,8 +285,8 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,
>
{i18n.str`Confirm withdrawal`}
</ButtonSuccess>
- }
- {terms.status === 'notfound' &&
+ )}
+ {terms.status === "notfound" && (
<ButtonWarning
upperCased
disabled={!exchangeBaseUrl}
@@ -231,60 +294,88 @@ export function View({ details, withdrawalFee, exchangeBaseUrl, knownExchanges,
>
{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
+export function WithdrawPageWithParsedURI({
+ uri,
+ uriInfo,
+}: {
+ uri: string;
+ uriInfo: WithdrawUriInfoResponse;
+}): VNode {
+ 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'])
+ 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 }
- })
+ tosAcceptedFormat: ["text/xml"],
+ });
+ return { tos, info };
+ });
if (!detailsHook) {
- return <span><i18n.Translate>Getting withdrawal details.</i18n.Translate></span>;
+ 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>;
+ return (
+ <span>
+ <i18n.Translate>
+ Problems getting details: {detailsHook.message}
+ </i18n.Translate>
+ </span>
+ );
}
- const details = detailsHook.response
+ const details = detailsHook.response;
const onAccept = async (): Promise<void> => {
try {
- await setExchangeTosAccepted(exchange, details.tos.currentEtag)
- setReviewed(true)
+ await setExchangeTosAccepted(exchange, details.tos.currentEtag);
+ setReviewed(true);
} catch (e) {
if (e instanceof Error) {
- setErrorAccepting(e.message)
+ setErrorAccepting(e.message);
}
}
- }
+ };
const onWithdraw = async (): Promise<void> => {
- setConfirmed(true)
+ setConfirmed(true);
console.log("accepting exchange", exchange);
try {
const res = await acceptWithdrawal(uri, exchange);
@@ -293,91 +384,121 @@ export function WithdrawPageWithParsedURI({ uri, uriInfo }: { uri: string, uriIn
document.location.href = res.confirmTransferUrl;
}
} catch (e) {
- setConfirmed(false)
+ 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}
- />
+ 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 })
- )
+export function WithdrawPage({ talerWithdrawUri }: Props): VNode {
+ const uriInfoHook = useAsyncAsHook(() =>
+ !talerWithdrawUri
+ ? Promise.reject(undefined)
+ : getWithdrawalDetailsForUri({ talerWithdrawUri }),
+ );
if (!talerWithdrawUri) {
- return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>;
+ return (
+ <span>
+ <i18n.Translate>missing withdraw uri</i18n.Translate>
+ </span>
+ );
}
if (!uriInfoHook) {
- return <span><i18n.Translate>Loading...</i18n.Translate></span>;
+ 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 (
+ <span>
+ <i18n.Translate>
+ This URI is not valid anymore: {uriInfoHook.message}
+ </i18n.Translate>
+ </span>
+ );
}
- return <WithdrawPageWithParsedURI uri={talerWithdrawUri} uriInfo={uriInfoHook.response} />
+ return (
+ <WithdrawPageWithParsedURI
+ uri={talerWithdrawUri}
+ uriInfo={uriInfoHook.response}
+ />
+ );
}
-function parseTermsOfServiceContent(type: string, text: string): TermsDocument | undefined {
- if (type === 'text/xml') {
+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 }
+ const document = new DOMParser().parseFromString(text, "text/xml");
+ return { type: "xml", document };
} catch (e) {
- console.log(e)
- debugger;
+ console.log(e);
}
- } else if (type === 'text/html') {
+ } else if (type === "text/html") {
try {
- const href = new URL(text)
- return { type: 'html', href }
+ const href = new URL(text);
+ return { type: "html", href };
} catch (e) {
- console.log(e)
- debugger;
+ console.log(e);
}
- } else if (type === 'text/json') {
+ } else if (type === "text/json") {
try {
- const data = JSON.parse(text)
- return { type: 'json', data }
+ const data = JSON.parse(text);
+ return { type: "json", data };
} catch (e) {
- console.log(e)
- debugger;
+ console.log(e);
}
- } else if (type === 'text/pdf') {
+ } else if (type === "text/pdf") {
try {
- const location = new URL(text)
- return { type: 'pdf', location }
+ const location = new URL(text);
+ return { type: "pdf", location };
} catch (e) {
- console.log(e)
- debugger;
+ console.log(e);
}
- } else if (type === 'text/plain') {
+ } else if (type === "text/plain") {
try {
- const content = text
- return { type: 'plain', content }
+ const content = text;
+ return { type: "plain", content };
} catch (e) {
- console.log(e)
- debugger;
+ console.log(e);
}
}
- return undefined
+ return undefined;
}
-
diff --git a/packages/taler-wallet-webextension/src/cta/payback.tsx b/packages/taler-wallet-webextension/src/cta/payback.tsx
index 1e27fd912..1c81b48a0 100644
--- a/packages/taler-wallet-webextension/src/cta/payback.tsx
+++ b/packages/taler-wallet-webextension/src/cta/payback.tsx
@@ -14,8 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { JSX } from "preact/jsx-runtime";
-import { h } from 'preact';
+import { h, VNode } from "preact";
/**
* View and edit auditors.
@@ -27,6 +26,6 @@ import { h } from 'preact';
* Imports.
*/
-export function makePaybackPage(): JSX.Element {
+export function makePaybackPage(): VNode {
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
index e66c0db57..75c4c1962 100644
--- a/packages/taler-wallet-webextension/src/cta/reset-required.tsx
+++ b/packages/taler-wallet-webextension/src/cta/reset-required.tsx
@@ -20,7 +20,7 @@
* @author Florian Dold
*/
-import { Component, JSX, h } from "preact";
+import { Component, h, VNode } from "preact";
import * as wxApi from "../wxApi";
interface State {
@@ -45,7 +45,7 @@ class ResetNotification extends Component<any, State> {
const res = await wxApi.checkUpgrade();
this.setState({ resetRequired: res.dbResetRequired });
}
- render(): JSX.Element {
+ render(): VNode {
if (this.state.resetRequired) {
return (
<div>
@@ -63,7 +63,7 @@ class ResetNotification extends Component<any, State> {
type="checkbox"
checked={this.state.checked}
onChange={() => {
- this.setState(prev => ({ checked: prev.checked }))
+ this.setState((prev) => ({ checked: prev.checked }));
}}
/>{" "}
<label htmlFor="check">
@@ -92,6 +92,6 @@ class ResetNotification extends Component<any, State> {
/**
* @deprecated to be removed
*/
-export function createResetRequiredPage(): JSX.Element {
+export function createResetRequiredPage(): VNode {
return <ResetNotification />;
}
diff --git a/packages/taler-wallet-webextension/src/cta/return-coins.tsx b/packages/taler-wallet-webextension/src/cta/return-coins.tsx
index 43d73b5fe..55f0297d4 100644
--- a/packages/taler-wallet-webextension/src/cta/return-coins.tsx
+++ b/packages/taler-wallet-webextension/src/cta/return-coins.tsx
@@ -14,8 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { JSX } from "preact/jsx-runtime";
-import { h } from 'preact';
+import { h, VNode } from "preact";
/**
* Return coins to own bank account.
*
@@ -25,6 +24,6 @@ import { h } from 'preact';
/**
* Imports.
*/
-export function createReturnCoinsPage(): JSX.Element {
+export function createReturnCoinsPage(): VNode {
return <span>Not implemented yet.</span>;
}
diff --git a/packages/taler-wallet-webextension/src/custom.d.ts b/packages/taler-wallet-webextension/src/custom.d.ts
index 1981067d4..521b824c7 100644
--- a/packages/taler-wallet-webextension/src/custom.d.ts
+++ b/packages/taler-wallet-webextension/src/custom.d.ts
@@ -21,7 +21,7 @@ declare module "*.png" {
const content: any;
export default content;
}
-declare module '*.svg' {
+declare module "*.svg" {
const content: any;
export default content;
}
diff --git a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
index 2131d45cb..aa6695c3e 100644
--- a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
@@ -29,7 +29,7 @@ interface HookError {
export type HookResponse<T> = HookOk<T> | HookError | undefined;
-export function useAsyncAsHook<T> (fn: (() => Promise<T>)): HookResponse<T> {
+export function useAsyncAsHook<T>(fn: () => Promise<T>): HookResponse<T> {
const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
useEffect(() => {
async function doAsync() {
@@ -42,7 +42,7 @@ export function useAsyncAsHook<T> (fn: (() => Promise<T>)): HookResponse<T> {
}
}
}
- doAsync()
+ doAsync();
}, []);
return result;
}
diff --git a/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts b/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts
index f3b1b3b5f..1aa711a90 100644
--- a/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useBackupDeviceName.ts
@@ -17,34 +17,31 @@
import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi";
-
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(),
+ });
useEffect(() => {
async function run() {
//create a first list of backup info by currency
- const status = await wxApi.getBackupInfo()
+ const status = await wxApi.getBackupInfo();
async function update(newName: string) {
- await wxApi.setWalletDeviceId(newName)
- setStatus(old => ({ ...old, name: newName }))
+ await wxApi.setWalletDeviceId(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
index c46ab6a5f..8a8fd6f2f 100644
--- a/packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useBackupStatus.ts
@@ -14,11 +14,15 @@
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 {
+ 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[];
@@ -32,40 +36,46 @@ function getStatusTypeOrder(t: ProviderPaymentStatus) {
ProviderPaymentType.Unpaid,
ProviderPaymentType.Paid,
ProviderPaymentType.Pending,
- ].indexOf(t.type)
+ ].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
+ 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)
+ 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 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)
+ if (
+ a.paymentStatus.type === ProviderPaymentType.Paid &&
+ b.paymentStatus.type === ProviderPaymentType.Paid
+ ) {
+ return getStatusPaidOrder(a.paymentStatus, b.paymentStatus);
}
- return getStatusTypeOrder(a.paymentStatus) - getStatusTypeOrder(b.paymentStatus)
- })
+ return (
+ getStatusTypeOrder(a.paymentStatus) -
+ getStatusTypeOrder(b.paymentStatus)
+ );
+ });
async function sync() {
- await wxApi.syncAllProviders()
+ await wxApi.syncAllProviders();
}
-
- setStatus({ deviceName: status.deviceId, providers, sync })
+
+ setStatus({ deviceName: status.deviceId, providers, sync });
}
- run()
- }, [])
+ run();
+ }, []);
- return status
+ return status;
}
-
-
diff --git a/packages/taler-wallet-webextension/src/hooks/useBalances.ts b/packages/taler-wallet-webextension/src/hooks/useBalances.ts
index 37424fb05..403ce7b87 100644
--- a/packages/taler-wallet-webextension/src/hooks/useBalances.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useBalances.ts
@@ -18,7 +18,6 @@ import { BalancesResponse } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi";
-
interface BalancesHookOk {
hasError: false;
response: BalancesResponse;
@@ -46,7 +45,7 @@ export function useBalances(): BalancesHook {
}
}
}
- checkBalance()
+ checkBalance();
return wxApi.onUpdateNotification(checkBalance);
}, []);
diff --git a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts
index 888d4d5f1..48aff2602 100644
--- a/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useDiagnostics.ts
@@ -21,7 +21,7 @@ import * as wxApi from "../wxApi";
export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {
const [timedOut, setTimedOut] = useState(false);
const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>(
- undefined
+ undefined,
);
useEffect(() => {
@@ -41,5 +41,5 @@ export function useDiagnostics(): [WalletDiagnostics | undefined, boolean] {
console.log("fetching diagnostics");
doFetch();
}, []);
- return [diagnostics, timedOut]
-} \ No newline at end of file
+ return [diagnostics, timedOut];
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts
index a92425760..aaab0aa43 100644
--- a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts
@@ -19,13 +19,12 @@ 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((v) => !v);
+ handleExtendedPerm(enabled).then((result) => {
setEnabled(result);
});
};
@@ -65,5 +64,5 @@ async function handleExtendedPerm(isEnabled: boolean): Promise<boolean> {
nextVal = res.newValue;
}
console.log("new permissions applied:", nextVal ?? false);
- return nextVal ?? false
-} \ No newline at end of file
+ return nextVal ?? false;
+}
diff --git a/packages/taler-wallet-webextension/src/hooks/useLang.ts b/packages/taler-wallet-webextension/src/hooks/useLang.ts
index 70b9614f6..cc4ff3fc8 100644
--- a/packages/taler-wallet-webextension/src/hooks/useLang.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useLang.ts
@@ -14,10 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { useNotNullLocalStorage } from './useLocalStorage';
+import { useNotNullLocalStorage } from "./useLocalStorage";
-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)
+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);
}
diff --git a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts b/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
index 78a8b65d5..3883aff04 100644
--- a/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useLocalStorage.ts
@@ -15,38 +15,52 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @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;
+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
+ 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)
+ window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, toStore);
}
}
- return toStore
- })
+ return toStore;
+ });
};
return [storedValue, setValue];
}
//TODO: merge with the above function
-export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
+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;
+ return typeof window !== "undefined"
+ ? window.localStorage.getItem(key) || initialValue
+ : initialValue;
});
const setValue = (value: string | ((val: string) => string)) => {
@@ -54,7 +68,7 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
if (!valueToStore) {
- window.localStorage.removeItem(key)
+ window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, valueToStore);
}
diff --git a/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts b/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
index 6520848a5..ea167463e 100644
--- a/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useProviderStatus.ts
@@ -32,7 +32,9 @@ export function useProviderStatus(url: string): ProviderStatus | undefined {
//create a first list of backup info by currency
const status = await wxApi.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() {
diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts
index ff9cc029a..96a278401 100644
--- a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts
@@ -17,15 +17,18 @@
import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
-export function useTalerActionURL(): [string | undefined, (s: boolean) => void] {
+export function useTalerActionURL(): [
+ string | undefined,
+ (s: boolean) => void,
+] {
const [talerActionUrl, setTalerActionUrl] = useState<string | undefined>(
- undefined
+ undefined,
);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
async function check(): Promise<void> {
const talerUri = await findTalerUriInActiveTab();
- setTalerActionUrl(talerUri)
+ setTalerActionUrl(talerUri);
}
check();
}, []);
diff --git a/packages/taler-wallet-webextension/src/i18n/strings.ts b/packages/taler-wallet-webextension/src/i18n/strings.ts
index 5b1257830..0fefb0f70 100644
--- a/packages/taler-wallet-webextension/src/i18n/strings.ts
+++ b/packages/taler-wallet-webextension/src/i18n/strings.ts
@@ -193,7 +193,7 @@ strings["es"] = {
"Order redirected": [""],
"Payment aborted": [""],
"Payment Sent": [""],
- "Backup": ["Resguardo"],
+ Backup: ["Resguardo"],
"Order accepted": [""],
"Reserve balance updated": [""],
"Payment refund": [""],
diff --git a/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx b/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx
index d256f6d98..232b0da73 100644
--- a/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Backup.stories.tsx
@@ -15,179 +15,184 @@
*/
/**
-*
-* @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 { 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',
+ title: "popup/backup/list",
component: TestedComponent,
argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
+ 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
+ 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",
+ },
},
- "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: {
+ 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",
+ },
},
- "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,
+ },
+ terms: {
+ annualFee: "KUDOS:0.1",
+ 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,
+ },
+ terms: {
+ annualFee: "KUDOS:0.1",
+ 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",
+ storageLimitInMegabytes: 8,
+ supportedProtocolVersion: "2",
+ },
+ oldTerms: {
+ annualFee: "USD:1",
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "1",
+ },
+ paidUntil: {
+ t_ms: "never",
+ },
+ },
+ terms: {
+ annualFee: "KUDOS:0.1",
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",
+ 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",
+ 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
- }
+ 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",
+ },
},
- "terms": {
- "annualFee": "ARS:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }]
+ ],
});
-
export const Empty = createExample(TestedComponent, {
- providers: []
+ providers: [],
});
-
diff --git a/packages/taler-wallet-webextension/src/popup/BackupPage.tsx b/packages/taler-wallet-webextension/src/popup/BackupPage.tsx
index dcc5e5313..ae93f8a40 100644
--- a/packages/taler-wallet-webextension/src/popup/BackupPage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BackupPage.tsx
@@ -14,15 +14,28 @@
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
+ ProviderInfo,
+ ProviderPaymentStatus,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ differenceInMonths,
+ formatDuration,
+ intervalToDuration,
+} from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import {
+ BoldLight,
+ ButtonPrimary,
+ ButtonSuccess,
+ Centered,
+ CenteredBoldText,
+ CenteredText,
+ PopupBox,
+ RowBorderGray,
+ SmallLightText,
+ SmallText,
} from "../components/styled";
import { useBackupStatus } from "../hooks/useBackupStatus";
import { Pages } from "../NavigationBar";
@@ -32,49 +45,69 @@ interface Props {
}
export function BackupPage({ onAddProvider }: Props): VNode {
- const status = useBackupStatus()
+ const status = useBackupStatus();
if (!status) {
- return <div>Loading...</div>
+ return <div>Loading...</div>;
}
- return <BackupView providers={status.providers} onAddProvider={onAddProvider} onSyncAll={status.sync} />;
+ return (
+ <BackupView
+ providers={status.providers}
+ onAddProvider={onAddProvider}
+ onSyncAll={status.sync}
+ />
+ );
}
export interface ViewProps {
- providers: ProviderInfo[],
+ providers: ProviderInfo[];
onAddProvider: () => void;
onSyncAll: () => Promise<void>;
}
-export function BackupView({ providers, onAddProvider, onSyncAll }: ViewProps): VNode {
+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.map((provider, idx) => (
+ <BackupLayout
+ key={idx}
+ 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>
)}
- {!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>}
+ {!!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 {
@@ -85,62 +118,80 @@ interface TransactionLayoutProps {
active: boolean;
}
-function BackupLayout(props: TransactionLayoutProps): JSX.Element {
+function BackupLayout(props: TransactionLayoutProps): VNode {
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>}
+ <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} /> :
+ {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>
+ 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)';
+ 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
+ if (d.t_ms === "never") return undefined;
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}`;
+}
diff --git a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
index 382f9b549..80203f6d3 100644
--- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
@@ -15,28 +15,25 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample, NullLink } from '../test-utils';
-import { BalanceView as TestedComponent } from './BalancePage';
+import { createExample, NullLink } from "../test-utils";
+import { BalanceView as TestedComponent } from "./BalancePage";
export default {
- title: 'popup/balance',
+ title: "popup/balance",
component: TestedComponent,
- argTypes: {
- }
+ argTypes: {},
};
-
-export const NotYetLoaded = createExample(TestedComponent, {
-});
+export const NotYetLoaded = createExample(TestedComponent, {});
export const GotError = createExample(TestedComponent, {
balance: {
hasError: true,
- message: 'Network error'
+ message: "Network error",
},
Linker: NullLink,
});
@@ -45,7 +42,7 @@ export const EmptyBalance = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
- balances: []
+ balances: [],
},
},
Linker: NullLink,
@@ -55,13 +52,15 @@ export const SomeCoins = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
- balances: [{
- available: 'USD:10.5',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- }]
+ balances: [
+ {
+ available: "USD:10.5",
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0",
+ pendingOutgoing: "USD:0",
+ requiresUserInput: false,
+ },
+ ],
},
},
Linker: NullLink,
@@ -71,13 +70,15 @@ 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
- }]
+ balances: [
+ {
+ available: "USD:2.23",
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:5.11",
+ pendingOutgoing: "USD:0",
+ requiresUserInput: false,
+ },
+ ],
},
},
Linker: NullLink,
@@ -87,13 +88,15 @@ 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
- }]
+ balances: [
+ {
+ available: "USD:2.23",
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0",
+ pendingOutgoing: "USD:5.11",
+ requiresUserInput: false,
+ },
+ ],
},
},
Linker: NullLink,
@@ -103,13 +106,15 @@ 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
- }]
+ balances: [
+ {
+ available: "USD:2.23",
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:2",
+ pendingOutgoing: "USD:5.11",
+ requiresUserInput: false,
+ },
+ ],
},
},
Linker: NullLink,
@@ -119,19 +124,22 @@ 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
- }]
+ 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,
+ },
+ ],
},
},
Linker: NullLink,
@@ -141,78 +149,89 @@ 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
- }]
+ 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,
+ },
+ ],
},
},
Linker: NullLink,
});
-
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
- }]
+ 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,
+ },
+ ],
},
},
Linker: NullLink,
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index 8e5c5c42e..a23c81cd1 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -15,20 +15,34 @@
*/
import {
- amountFractionalBase, Amounts,
- Balance, BalancesResponse,
- i18n
+ amountFractionalBase,
+ Amounts,
+ Balance,
+ i18n,
} 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 { h, VNode } from "preact";
+import {
+ ButtonPrimary,
+ ErrorBox,
+ Middle,
+ PopupBox,
+} 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 function BalancePage({
+ goToWalletManualWithdraw,
+}: {
+ goToWalletManualWithdraw: () => void;
+}): VNode {
+ const balance = useBalances();
+ return (
+ <BalanceView
+ balance={balance}
+ Linker={PageLink}
+ goToWalletManualWithdraw={goToWalletManualWithdraw}
+ />
+ );
}
export interface BalanceViewProps {
balance: BalancesHook;
@@ -36,32 +50,36 @@ export interface BalanceViewProps {
goToWalletManualWithdraw: () => void;
}
-function formatPending(entry: Balance): JSX.Element {
- let incoming: JSX.Element | undefined;
- let payment: JSX.Element | undefined;
+function formatPending(entry: Balance): VNode {
+ let incoming: VNode | undefined;
+ let payment: VNode | undefined;
- const available = Amounts.parseOrThrow(entry.available);
+ // 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>
+ <span>
+ <i18n.Translate>
+ <span style={{ color: "darkgreen" }} title="incoming amount">
+ {"+"}
+ {renderAmount(entry.pendingIncoming)}
+ </span>{" "}
+ </i18n.Translate>
+ </span>
);
}
if (!Amounts.isZero(pendingOutgoing)) {
payment = (
- <span><i18n.Translate>
- <span style={{ color: "darkred" }} title="outgoing amount">
- {"-"}
- {renderAmount(entry.pendingOutgoing)}
- </span>{" "}
- </i18n.Translate></span>
+ <span>
+ <i18n.Translate>
+ <span style={{ color: "darkred" }} title="outgoing amount">
+ {"-"}
+ {renderAmount(entry.pendingOutgoing)}
+ </span>{" "}
+ </i18n.Translate>
+ </span>
);
}
@@ -80,76 +98,110 @@ function formatPending(entry: Balance): JSX.Element {
);
}
-
-export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) {
-
- function Content() {
+export function BalanceView({
+ balance,
+ Linker,
+ goToWalletManualWithdraw,
+}: BalanceViewProps): VNode {
+ function Content(): VNode {
if (!balance) {
- return <span />
+ return <span />;
}
if (balance.hasError) {
- return (<section>
- <ErrorBox>{balance.message}</ErrorBox>
- <p>
- Click <Linker pageName="welcome">here</Linker> for help and
- diagnostics.
- </p>
- </section>)
+ 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>
+ <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>
+ return (
+ <section data-expanded data-centered>
+ <table style={{ width: "100%" }}>
+ {balance.response.balances.map((entry, idx) => {
+ 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 key={idx}>
+ <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>
+ );
}
- return <PopupBox>
- {/* <section> */}
- <Content />
- {/* </section> */}
- <footer>
- <div />
- <ButtonPrimary onClick={goToWalletManualWithdraw}>Withdraw</ButtonPrimary>
- </footer>
- </PopupBox>
+ return (
+ <PopupBox>
+ {/* <section> */}
+ <Content />
+ {/* </section> */}
+ <footer>
+ <div />
+ <ButtonPrimary onClick={goToWalletManualWithdraw}>
+ Withdraw
+ </ButtonPrimary>
+ </footer>
+ </PopupBox>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/popup/Debug.tsx b/packages/taler-wallet-webextension/src/popup/Debug.tsx
index ccc747466..b0e8543fc 100644
--- a/packages/taler-wallet-webextension/src/popup/Debug.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Debug.tsx
@@ -14,18 +14,19 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { JSX, h } from "preact";
+import { h, VNode } 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 {
+export function DeveloperPage(): VNode {
const [status, timedOut] = useDiagnostics();
return (
<div>
<p>Debug tools:</p>
- <button onClick={openExtensionPage("/static/popup.html")}>wallet tab</button>
+ <button onClick={openExtensionPage("/static/popup.html")}>
+ wallet tab
+ </button>
<br />
<button onClick={confirmReset}>reset</button>
<Diagnostics diagnostics={status} timedOut={timedOut} />
@@ -35,6 +36,7 @@ export function DeveloperPage(props: any): JSX.Element {
export function reload(): void {
try {
+ // eslint-disable-next-line no-undef
chrome.runtime.reload();
window.close();
} catch (e) {
@@ -46,7 +48,7 @@ export async function confirmReset(): Promise<void> {
if (
confirm(
"Do you want to IRREVOCABLY DESTROY everything inside your" +
- " wallet and LOSE ALL YOUR COINS?",
+ " wallet and LOSE ALL YOUR COINS?",
)
) {
await wxApi.resetDb();
@@ -56,9 +58,10 @@ export async function confirmReset(): Promise<void> {
export function openExtensionPage(page: string) {
return () => {
+ // eslint-disable-next-line no-undef
chrome.tabs.create({
+ // eslint-disable-next-line no-undef
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
index daa263a81..95f4a547a 100644
--- a/packages/taler-wallet-webextension/src/popup/History.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/History.stories.tsx
@@ -15,135 +15,149 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import {
PaymentStatus,
- TransactionCommon, TransactionDeposit, TransactionPayment,
- TransactionRefresh, TransactionRefund, TransactionTip, TransactionType,
+ 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';
+ WithdrawalType,
+} from "@gnu-taler/taler-util";
+import { createExample } from "../test-utils";
+import { HistoryView as TestedComponent } from "./History";
export default {
- title: 'popup/history/list',
+ title: "popup/history/list",
component: TestedComponent,
};
const commonTransaction = {
- amountRaw: 'USD:10',
- amountEffective: 'USD:9',
+ amountRaw: "USD:10",
+ amountEffective: "USD:9",
pending: false,
timestamp: {
- t_ms: new Date().getTime()
+ t_ms: new Date().getTime(),
},
- transactionId: '12',
-} as TransactionCommon
+ transactionId: "12",
+} as TransactionCommon;
const exampleData = {
withdraw: {
...commonTransaction,
type: TransactionType.Withdrawal,
- exchangeBaseUrl: 'http://exchange.demo.taler.net',
+ exchangeBaseUrl: "http://exchange.demo.taler.net",
withdrawalDetails: {
confirmed: false,
- exchangePaytoUris: ['payto://x-taler-bank/bank/account'],
+ exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
type: WithdrawalType.ManualTransfer,
- }
+ },
} as TransactionWithdrawal,
payment: {
...commonTransaction,
- amountEffective: 'USD:11',
+ amountEffective: "USD:11",
type: TransactionType.Payment,
info: {
- contractTermsHash: 'ASDZXCASD',
+ contractTermsHash: "ASDZXCASD",
merchant: {
- name: 'the merchant',
+ name: "the merchant",
},
- orderId: '2021.167-03NPY6MCYMVGT',
+ orderId: "2021.167-03NPY6MCYMVGT",
products: [],
- summary: 'the summary',
- fulfillmentMessage: '',
+ summary: "the summary",
+ fulfillmentMessage: "",
},
- proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
+ proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
} 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',
+ exchangeBaseUrl: "http://exchange.taler",
} as TransactionRefresh,
tip: {
...commonTransaction,
type: TransactionType.Tip,
- merchantBaseUrl: 'http://merchant.taler',
+ merchantBaseUrl: "http://merchant.taler",
} as TransactionTip,
refund: {
...commonTransaction,
type: TransactionType.Refund,
- refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
+ refundedTransactionId:
+ "payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
info: {
- contractTermsHash: 'ASDZXCASD',
+ contractTermsHash: "ASDZXCASD",
merchant: {
- name: 'the merchant',
+ name: "the merchant",
},
- orderId: '2021.167-03NPY6MCYMVGT',
+ orderId: "2021.167-03NPY6MCYMVGT",
products: [],
- summary: 'the summary',
- fulfillmentMessage: '',
+ 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,
- }]
+ balances: [
+ {
+ available: "TESTKUDOS:10",
+ pendingIncoming: "TESTKUDOS:0",
+ pendingOutgoing: "TESTKUDOS:0",
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ },
+ ],
});
export const EmptyWithNoBalance = createExample(TestedComponent, {
list: [],
- balances: []
+ balances: [],
});
export const One = createExample(TestedComponent, {
list: [exampleData.withdraw],
- balances: [{
- available: 'USD:10',
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
+ 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,
- }]
+ list: [
+ {
+ ...exampleData.withdraw,
+ pending: true,
+ },
+ ],
+ balances: [
+ {
+ available: "USD:10",
+ pendingIncoming: "USD:0",
+ pendingOutgoing: "USD:0",
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ },
+ ],
});
export const Several = createExample(TestedComponent, {
@@ -157,13 +171,15 @@ export const Several = createExample(TestedComponent, {
exampleData.tip,
exampleData.deposit,
],
- balances: [{
- available: 'TESTKUDOS:10',
- pendingIncoming: 'TESTKUDOS:0',
- pendingOutgoing: 'TESTKUDOS:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
+ balances: [
+ {
+ available: "TESTKUDOS:10",
+ pendingIncoming: "TESTKUDOS:0",
+ pendingOutgoing: "TESTKUDOS:0",
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ },
+ ],
});
export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
@@ -177,18 +193,20 @@ export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
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,
- }]
+ 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
index 1447da9b0..2228271dc 100644
--- a/packages/taler-wallet-webextension/src/popup/History.tsx
+++ b/packages/taler-wallet-webextension/src/popup/History.tsx
@@ -14,21 +14,28 @@
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 {
+ AmountString,
+ Balance,
+ i18n,
+ Transaction,
+ TransactionsResponse,
+} from "@gnu-taler/taler-util";
+import { h, VNode } 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 {
+export function HistoryPage(): VNode {
const [transactions, setTransactions] = useState<
TransactionsResponse | undefined
>(undefined);
- const balance = useBalances()
- const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || [])
+ const balance = useBalances();
+ const balanceWithoutError = balance?.hasError
+ ? []
+ : balance?.response.balances || [];
useEffect(() => {
const fetchData = async (): Promise<void> => {
@@ -42,46 +49,79 @@ export function HistoryPage(props: any): JSX.Element {
return <div>Loading ...</div>;
}
- return <HistoryView balances={balanceWithoutError} list={[...transactions.transactions].reverse()} />;
+ 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)}`
+ 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>
+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/Popup.stories.tsx b/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx
index cd443e9d4..5009684c5 100644
--- a/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Popup.stories.tsx
@@ -15,30 +15,29 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { NavBar as TestedComponent } from '../NavigationBar';
+import { createExample } from "../test-utils";
+import { NavBar as TestedComponent } from "../NavigationBar";
export default {
- title: 'popup/header',
+ title: "popup/header",
// component: TestedComponent,
argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
+ onRetry: { action: "onRetry" },
+ onDelete: { action: "onDelete" },
+ onBack: { action: "onBack" },
+ },
};
-
export const OnBalance = createExample(TestedComponent, {
- devMode:false,
- path:'/balance'
+ devMode: false,
+ path: "/balance",
});
export const OnHistoryWithDevMode = createExample(TestedComponent, {
- devMode:true,
- path:'/history'
+ devMode: true,
+ path: "/history",
});
diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx
index de1f67b96..0cff7f75f 100644
--- a/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx
@@ -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 { createExample } from "../test-utils";
+import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage";
export default {
- title: 'popup/backup/confirm',
+ title: "popup/backup/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/',
+ 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/',
+ 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/popup/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx
index 2daf49e0c..9a2f97051 100644
--- a/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx
@@ -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 { createExample } from "../test-utils";
+import { SetUrlView as TestedComponent } from "./ProviderAddPage";
export default {
- title: 'popup/backup/add',
+ title: "popup/backup/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 = createExample(TestedComponent, {
-});
+export const Initial = createExample(TestedComponent, {});
export const WithValue = createExample(TestedComponent, {
- initialValue: 'sync.demo.taler.net'
-});
+ initialValue: "sync.demo.taler.net",
+});
export const WithConnectionError = createExample(TestedComponent, {
- withError: 'Network error'
-});
+ withError: "Network error",
+});
export const WithClientError = createExample(TestedComponent, {
- withError: 'URL may not be right: (404) Not Found'
-});
+ withError: "URL may not be right: (404) Not Found",
+});
export const WithServerError = createExample(TestedComponent, {
- withError: 'Try another server: (500) Internal Server Error'
-});
+ 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
index 4416608f8..fab21398a 100644
--- a/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx
@@ -15,224 +15,221 @@
*/
/**
-*
-* @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 { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
+import { createExample } from "../test-utils";
+import { ProviderView as TestedComponent } from "./ProviderDetailPage";
export default {
- title: 'popup/backup/details',
+ title: "popup/backup/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, {
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: {
+ t_ms: 1625063925078,
+ },
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
+ 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
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp: {
+ t_ms: 1625063925078,
},
lastAttemptedBackupTimestamp: {
- "t_ms": 1625063925078
+ t_ms: 1625063925078,
},
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
+ 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"
- }
- }
+ 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"
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp: {
+ t_ms: 1625063925078,
+ },
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: {
+ t_ms: 1656599921000,
+ },
},
backupProblem: {
- type: 'backup-unreadable'
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
+ 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"
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp: {
+ t_ms: 1625063925078,
+ },
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: {
+ t_ms: 1656599921000,
+ },
},
backupProblem: {
- type: 'backup-conflicting-device',
- myDeviceId: 'my-device-id',
- otherDeviceId: 'other-device-id',
+ 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"
- }
- }
+ 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"
- }
- }
+ 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"
- }
- }
+ 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"
- }
- }
+ 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,
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.TermsChanged,
paidUntil: {
- t_ms: 1656599921000
+ t_ms: 1656599921000,
},
newTerms: {
- "annualFee": "EUR:10",
- "storageLimitInMegabytes": 8,
- "supportedProtocolVersion": "0.0"
+ 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"
- }
- }
+ 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
index 04adbb21c..9617c9a41 100644
--- a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx
@@ -14,13 +14,23 @@
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 {
+ 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 {
+ Button,
+ ButtonDestructive,
+ ButtonPrimary,
+ PaymentStatus,
+ PopupBox,
+ SmallLightText,
+} from "../components/styled";
import { useProviderStatus } from "../hooks/useProviderStatus";
interface Props {
@@ -29,20 +39,29 @@ interface Props {
}
export function ProviderDetailPage({ pid, onBack }: Props): VNode {
- const status = useProviderStatus(pid)
+ const status = useProviderStatus(pid);
if (!status) {
- return <div><i18n.Translate>Loading...</i18n.Translate></div>
+ return (
+ <div>
+ <i18n.Translate>Loading...</i18n.Translate>
+ </div>
+ );
}
if (!status.info) {
- onBack()
- return <div />
+ onBack();
+ return <div />;
}
- return <ProviderView info={status.info}
- onSync={status.sync}
- onDelete={() => status.remove().then(onBack)}
- onBack={onBack}
- onExtend={() => { null }}
- />;
+ return (
+ <ProviderView
+ info={status.info}
+ onSync={status.sync}
+ onDelete={() => status.remove().then(onBack)}
+ onBack={onBack}
+ onExtend={() => {
+ null;
+ }}
+ />
+ );
}
export interface ViewProps {
@@ -53,124 +72,185 @@ export interface ViewProps {
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
+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>
+ <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>
+ <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>}
+ <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>
+ <Button onClick={onBack}>
+ <i18n.Translate> &lt; back</i18n.Translate>
+ </Button>
<div>
- <ButtonDestructive onClick={onDelete}><i18n.Translate>remove provider</i18n.Translate></ButtonDestructive>
+ <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'
+ if (!d || d.t_ms === "never") return "never synced";
const duration = intervalToDuration({
start: d.t_ms,
end: new Date(),
- })
+ });
const str = formatDuration(duration, {
- delimiter: ', ',
+ 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`
+ 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} />
+ 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>} />
+ 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" />
+ 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 (
+ <ErrorMessage
+ title={
+ <Fragment>
+ <i18n.Translate>
+ Unknown backup problem: {JSON.stringify(info.backupProblem)}
+ </i18n.Translate>
+ </Fragment>
+ }
+ />
+ );
}
}
- return null
+ return null;
}
function colorByStatus(status: ProviderPaymentType) {
switch (status) {
case ProviderPaymentType.InsufficientBalance:
- return 'rgb(223, 117, 20)'
+ return "rgb(223, 117, 20)";
case ProviderPaymentType.Unpaid:
- return 'rgb(202, 60, 60)'
+ return "rgb(202, 60, 60)";
case ProviderPaymentType.Paid:
- return 'rgb(28, 184, 65)'
+ return "rgb(28, 184, 65)";
case ProviderPaymentType.Pending:
- return 'gray'
+ return "gray";
case ProviderPaymentType.InsufficientBalance:
- return 'rgb(202, 60, 60)'
+ return "rgb(202, 60, 60)";
case ProviderPaymentType.TermsChanged:
- return 'rgb(202, 60, 60)'
+ return "rgb(202, 60, 60)";
}
}
@@ -180,16 +260,19 @@ function descriptionByStatus(status: ProviderPaymentStatus) {
// return i18n.str`not paid yet`
case ProviderPaymentType.Paid:
case ProviderPaymentType.TermsChanged:
- if (status.paidUntil.t_ms === 'never') {
- return i18n.str`service paid`
+ 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>
+ 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 ''
+ return "";
}
}
diff --git a/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx b/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx
index 06e33c9d3..ae8e54ba1 100644
--- a/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx
@@ -15,29 +15,28 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { SettingsView as TestedComponent } from './Settings';
+import { createExample } from "../test-utils";
+import { SettingsView as TestedComponent } from "./Settings";
export default {
- title: 'popup/settings',
+ title: "popup/settings",
component: TestedComponent,
argTypes: {
setDeviceName: () => Promise.resolve(),
- }
+ },
};
export const AllOff = createExample(TestedComponent, {
- deviceName: 'this-is-the-device-name',
+ deviceName: "this-is-the-device-name",
setDeviceName: () => Promise.resolve(),
});
export const OneChecked = createExample(TestedComponent, {
- deviceName: 'this-is-the-device-name',
+ deviceName: "this-is-the-device-name",
permissionsEnabled: true,
setDeviceName: () => Promise.resolve(),
});
-
diff --git a/packages/taler-wallet-webextension/src/popup/Settings.tsx b/packages/taler-wallet-webextension/src/popup/Settings.tsx
index 8595c87ff..3b83f0762 100644
--- a/packages/taler-wallet-webextension/src/popup/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Settings.tsx
@@ -14,7 +14,6 @@
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";
@@ -28,15 +27,21 @@ 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}
- />;
+ 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 {
@@ -50,23 +55,31 @@ export interface ViewProps {
toggleDeveloperMode: () => void;
}
-import { strings as messages } from '../i18n/strings'
+import { strings as messages } from "../i18n/strings";
type LangsNames = {
- [P in keyof typeof messages]: string
-}
+ [P in keyof typeof messages]: string;
+};
const names: LangsNames = {
- es: 'Español [es]',
- en: 'English [en]',
- fr: 'Français [fr]',
- de: 'Deutsch [de]',
- sv: 'Svenska [sv]',
- it: 'Italiano [it]',
-}
-
+ es: "Español [es]",
+ en: "English [en]",
+ fr: "Français [fr]",
+ de: "Deutsch [de]",
+ sv: "Svenska [sv]",
+ it: "Italiano [it]",
+};
-export function SettingsView({ lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode {
+export function SettingsView({
+ lang,
+ changeLang,
+ deviceName,
+ setDeviceName,
+ permissionsEnabled,
+ togglePermissions,
+ developerMode,
+ toggleDeveloperMode,
+}: ViewProps): VNode {
return (
<PopupBox>
<section>
@@ -86,25 +99,39 @@ export function SettingsView({ lang, changeLang, deviceName, setDeviceName, perm
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"
+ <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}
+ enabled={permissionsEnabled}
+ onToggle={togglePermissions}
/>
<h2>Config</h2>
- <Checkbox label="Developer mode"
+ <Checkbox
+ label="Developer mode"
name="devMode"
description="(More options and information useful for debugging)"
- enabled={developerMode} onToggle={toggleDeveloperMode}
+ enabled={developerMode}
+ onToggle={toggleDeveloperMode}
/>
</section>
- <footer style={{ justifyContent: 'space-around' }}>
- <a target="_blank"
+ <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>
+ 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..f20403d6a 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.stories.tsx
@@ -15,38 +15,38 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { TalerActionFound as TestedComponent } from './TalerActionFound';
+import { createExample } from "../test-utils";
+import { TalerActionFound as TestedComponent } from "./TalerActionFound";
export default {
- title: 'popup/TalerActionFound',
+ title: "popup/TalerActionFound",
component: TestedComponent,
};
export const PayAction = createExample(TestedComponent, {
- url: 'taler://pay/something'
+ url: "taler://pay/something",
});
export const WithdrawalAction = createExample(TestedComponent, {
- url: 'taler://withdraw/something'
+ url: "taler://withdraw/something",
});
export const TipAction = createExample(TestedComponent, {
- url: 'taler://tip/something'
+ url: "taler://tip/something",
});
export const NotifyAction = createExample(TestedComponent, {
- url: 'taler://notify-reserve/something'
+ url: "taler://notify-reserve/something",
});
export const RefundAction = createExample(TestedComponent, {
- url: 'taler://refund/something'
+ url: "taler://refund/something",
});
export const InvalidAction = createExample(TestedComponent, {
- url: 'taler://something/asd'
+ 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..cbdcbeb15 100644
--- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
+++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx
@@ -1,5 +1,31 @@
+/*
+ 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 { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
-import { ButtonPrimary, ButtonSuccess, PopupBox } from "../components/styled/index";
+import {
+ ButtonPrimary,
+ ButtonSuccess,
+ PopupBox,
+} from "../components/styled/index";
+import { h } from "preact";
export interface Props {
url: string;
@@ -8,54 +34,89 @@ export interface Props {
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>;
-
+ 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 actionForTalerUri(uriType: TalerUriType, talerUri: string): string | undefined {
+function actionForTalerUri(
+ uriType: TalerUriType,
+ talerUri: string,
+): string | undefined {
switch (uriType) {
case TalerUriType.TalerWithdraw:
return makeExtensionUrlWithParams("static/wallet.html#/withdraw", {
@@ -91,8 +152,10 @@ function makeExtensionUrlWithParams(
): 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
+ const hParams = Object.keys(params)
+ .map((k) => `${k}=${params[k]}`)
+ .join("&");
+ innerUrl.hash = innerUrl.hash + "?" + hParams;
}
return innerUrl.href;
}
diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
index 070df554c..a5723ccb5 100644
--- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
@@ -22,19 +22,17 @@
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 { render, h } from "preact";
+import Router, { route, Route } from "preact-router";
+import { useEffect } from "preact/hooks";
import { DevContextProvider } from "./context/devContext";
import { useTalerActionURL } from "./hooks/useTalerActionURL";
import { strings } from "./i18n/strings";
+import { Pages, WalletNavBar } from "./NavigationBar";
import { BackupPage } from "./popup/BackupPage";
import { BalancePage } from "./popup/BalancePage";
-import { DeveloperPage as DeveloperPage } from "./popup/Debug";
+import { 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";
@@ -64,11 +62,11 @@ if (document.readyState === "loading") {
}
function Application() {
- const [talerActionUrl, setDismissed] = useTalerActionURL()
+ const [talerActionUrl, setDismissed] = useTalerActionURL();
useEffect(() => {
- if (talerActionUrl) route(Pages.cta)
- },[talerActionUrl])
+ if (talerActionUrl) route(Pages.cta);
+ }, [talerActionUrl]);
return (
<div>
@@ -78,33 +76,54 @@ function Application() {
<Router history={createHashHistory()}>
<Route path={Pages.dev} component={DeveloperPage} />
- <Route path={Pages.balance} component={BalancePage}
- goToWalletManualWithdraw={() => goToWalletPage(Pages.manual_withdraw)}
+ <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.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.transaction}
+ component={({ tid }: { tid: string }) =>
+ goToWalletPage(Pages.transaction.replace(":tid", tid))
+ }
/>
<Route path={Pages.history} component={HistoryPage} />
- <Route path={Pages.backup} component={BackupPage}
+ <Route
+ path={Pages.backup}
+ component={BackupPage}
onAddProvider={() => {
- route(Pages.provider_add)
+ route(Pages.provider_add);
}}
/>
- <Route path={Pages.provider_detail} component={ProviderDetailPage}
+ <Route
+ path={Pages.provider_detail}
+ component={ProviderDetailPage}
onBack={() => {
- route(Pages.backup)
+ route(Pages.backup);
}}
/>
- <Route path={Pages.provider_add} component={ProviderAddPage}
+ <Route
+ path={Pages.provider_add}
+ component={ProviderAddPage}
onBack={() => {
- route(Pages.backup)
+ route(Pages.backup);
}}
/>
<Route default component={Redirect} to={Pages.balance} />
@@ -119,13 +138,13 @@ function goToWalletPage(page: Pages | string): null {
chrome.tabs.create({
active: true,
url: chrome.extension.getURL(`/static/wallet.html#${page}`),
- })
- return null
+ });
+ return null;
}
function Redirect({ to }: { to: string }): null {
useEffect(() => {
- route(to, true)
- })
- return null
+ route(to, true);
+ });
+ return null;
}
diff --git a/packages/taler-wallet-webextension/src/renderHtml.tsx b/packages/taler-wallet-webextension/src/renderHtml.tsx
index bbe8e465c..15986d5d1 100644
--- a/packages/taler-wallet-webextension/src/renderHtml.tsx
+++ b/packages/taler-wallet-webextension/src/renderHtml.tsx
@@ -28,13 +28,13 @@ import {
Amounts,
amountFractionalBase,
} from "@gnu-taler/taler-util";
-import { Component, ComponentChildren, JSX, h } from "preact";
+import { Component, ComponentChildren, h, VNode } from "preact";
/**
* Render amount as HTML, which non-breaking space between
* decimal value and currency.
*/
-export function renderAmount(amount: AmountJson | string): JSX.Element {
+export function renderAmount(amount: AmountJson | string): VNode {
let a;
if (typeof amount === "string") {
a = Amounts.parse(amount);
@@ -56,13 +56,13 @@ export const AmountView = ({
amount,
}: {
amount: AmountJson | string;
-}): JSX.Element => renderAmount(amount);
+}): VNode => 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 {
+export function abbrev(s: string, n = 5): VNode {
let sAbbrev = s;
if (s.length > n) {
sAbbrev = s.slice(0, n) + "..";
@@ -87,15 +87,12 @@ interface CollapsibleProps {
* Component that shows/hides its children when clicking
* a heading.
*/
-export class Collapsible extends Component<
- CollapsibleProps,
- CollapsibleState
-> {
+export class Collapsible extends Component<CollapsibleProps, CollapsibleState> {
constructor(props: CollapsibleProps) {
super(props);
this.state = { collapsed: props.initiallyCollapsed };
}
- render(): JSX.Element {
+ render(): VNode {
const doOpen = (e: any): void => {
this.setState({ collapsed: false });
e.preventDefault();
@@ -135,27 +132,24 @@ interface ExpanderTextProps {
/**
* Show a heading with a toggle to show/hide the expandable content.
*/
-export function ExpanderText({ text }: ExpanderTextProps): JSX.Element {
+export function ExpanderText({ text }: ExpanderTextProps): VNode {
return <span>{text}</span>;
}
-export interface LoadingButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
+export interface LoadingButtonProps
+ extends h.JSX.HTMLAttributes<HTMLButtonElement> {
isLoading: boolean;
}
-export function ProgressButton({isLoading, ...rest}: LoadingButtonProps): JSX.Element {
+export function ProgressButton({
+ isLoading,
+ ...rest
+}: LoadingButtonProps): VNode {
return (
- <button
- class="pure-button pure-button-primary"
- type="button"
- {...rest}
- >
+ <button class="pure-button pure-button-primary" type="button" {...rest}>
{isLoading ? (
<span>
- <object
- class="svg-icon svg-baseline"
- data="/img/spinner-bars.svg"
- />
+ <object class="svg-icon svg-baseline" data="/img/spinner-bars.svg" />
</span>
) : null}{" "}
{rest.children}
@@ -163,17 +157,14 @@ export function ProgressButton({isLoading, ...rest}: LoadingButtonProps): JSX.El
);
}
-export function PageLink(
- props: { pageName: string, children?: ComponentChildren },
-): JSX.Element {
+export function PageLink(props: {
+ pageName: string;
+ children?: ComponentChildren;
+}): VNode {
+ // eslint-disable-next-line no-undef
const url = chrome.extension.getURL(`/static/wallet.html#/${props.pageName}`);
return (
- <a
- class="actionLink"
- href={url}
- target="_blank"
- rel="noopener noreferrer"
- >
+ <a class="actionLink" href={url} target="_blank" rel="noopener noreferrer">
{props.children}
</a>
);
diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts
index 6bf1be3ff..28622bb85 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -14,15 +14,17 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { ComponentChildren, FunctionalComponent, h as render } from 'preact';
+import { ComponentChildren, FunctionalComponent, h as render } from "preact";
-export function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
- const r = (args: any) => render(Component, args)
- r.args = props
- return r
+export function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props>,
+) {
+ const r = (args: any) => render(Component, args);
+ r.args = props;
+ return r;
}
-
export function NullLink({ children }: { children?: ComponentChildren }) {
- return render('a', { children, href: 'javascript:void(0);' })
+ return render("a", { children, href: "javascript:void(0);" });
}
diff --git a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
index 9a53fefe2..b2771bc2a 100644
--- a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx
@@ -15,179 +15,184 @@
*/
/**
-*
-* @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 { 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: 'wallet/backup/list',
+ title: "wallet/backup/list",
component: TestedComponent,
argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
+ 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
+ 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",
+ },
},
- "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: {
+ 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",
+ },
},
- "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,
+ },
+ terms: {
+ annualFee: "KUDOS:0.1",
+ 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,
+ },
+ terms: {
+ annualFee: "KUDOS:0.1",
+ 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",
+ storageLimitInMegabytes: 8,
+ supportedProtocolVersion: "2",
+ },
+ oldTerms: {
+ annualFee: "USD:1",
+ storageLimitInMegabytes: 16,
+ supportedProtocolVersion: "1",
+ },
+ paidUntil: {
+ t_ms: "never",
+ },
+ },
+ terms: {
+ annualFee: "KUDOS:0.1",
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",
+ 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",
+ 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
- }
+ 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",
+ },
},
- "terms": {
- "annualFee": "ARS:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }]
+ ],
});
-
export const Empty = createExample(TestedComponent, {
- providers: []
+ providers: [],
});
-
diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
index 712329bf8..f0ae38e0f 100644
--- a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
@@ -14,15 +14,28 @@
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
+ ProviderInfo,
+ ProviderPaymentStatus,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ differenceInMonths,
+ formatDuration,
+ intervalToDuration,
+} from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import {
+ BoldLight,
+ ButtonPrimary,
+ ButtonSuccess,
+ Centered,
+ CenteredBoldText,
+ CenteredText,
+ RowBorderGray,
+ SmallLightText,
+ SmallText,
+ WalletBox,
} from "../components/styled";
import { useBackupStatus } from "../hooks/useBackupStatus";
import { Pages } from "../NavigationBar";
@@ -32,49 +45,69 @@ interface Props {
}
export function BackupPage({ onAddProvider }: Props): VNode {
- const status = useBackupStatus()
+ const status = useBackupStatus();
if (!status) {
- return <div>Loading...</div>
+ return <div>Loading...</div>;
}
- return <BackupView providers={status.providers} onAddProvider={onAddProvider} onSyncAll={status.sync} />;
+ return (
+ <BackupView
+ providers={status.providers}
+ onAddProvider={onAddProvider}
+ onSyncAll={status.sync}
+ />
+ );
}
export interface ViewProps {
- providers: ProviderInfo[],
+ providers: ProviderInfo[];
onAddProvider: () => void;
onSyncAll: () => Promise<void>;
}
-export function BackupView({ providers, onAddProvider, onSyncAll }: ViewProps): VNode {
+export function BackupView({
+ providers,
+ onAddProvider,
+ onSyncAll,
+}: ViewProps): VNode {
return (
<WalletBox>
<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}
+ 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>
)}
- {!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>}
+ {!!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>
- )
+ );
}
interface TransactionLayoutProps {
@@ -85,62 +118,80 @@ interface TransactionLayoutProps {
active: boolean;
}
-function BackupLayout(props: TransactionLayoutProps): JSX.Element {
+function BackupLayout(props: TransactionLayoutProps): VNode {
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>}
+ <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} /> :
+ {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>
+ 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)';
+ 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
+ if (d.t_ms === "never") return undefined;
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}`;
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx
index cccda203e..2432c31eb 100644
--- a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx
@@ -15,28 +15,25 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample, NullLink } from '../test-utils';
-import { BalanceView as TestedComponent } from './BalancePage';
+import { createExample, NullLink } from "../test-utils";
+import { BalanceView as TestedComponent } from "./BalancePage";
export default {
- title: 'wallet/balance',
+ title: "wallet/balance",
component: TestedComponent,
- argTypes: {
- }
+ argTypes: {},
};
-
-export const NotYetLoaded = createExample(TestedComponent, {
-});
+export const NotYetLoaded = createExample(TestedComponent, {});
export const GotError = createExample(TestedComponent, {
balance: {
hasError: true,
- message: 'Network error'
+ message: "Network error",
},
Linker: NullLink,
});
@@ -45,7 +42,7 @@ export const EmptyBalance = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
- balances: []
+ balances: [],
},
},
Linker: NullLink,
@@ -55,13 +52,15 @@ export const SomeCoins = createExample(TestedComponent, {
balance: {
hasError: false,
response: {
- balances: [{
- available: 'USD:10.5',
- hasPendingTransactions: false,
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- requiresUserInput: false
- }]
+ balances: [
+ {
+ available: "USD:10.5",
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:0",
+ pendingOutgoing: "USD:0",
+ requiresUserInput: false,
+ },
+ ],
},
},
Linker: NullLink,
@@ -71,13 +70,15 @@ 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
- }]
+ balances: [
+ {
+ available: "USD:2.23",
+ hasPendingTransactions: false,
+ pendingIncoming: "USD:5.11",
+ pendingOutgoing: "USD:0",
+ requiresUserInput: false,
+ },
+ ],
},
},
Linker: NullLink,
@@ -87,19 +88,22 @@ 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
- }]
+ 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
index eb5a0447c..9a2847670 100644
--- a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
@@ -15,19 +15,30 @@
*/
import {
- amountFractionalBase, Amounts,
- Balance, BalancesResponse,
- i18n
+ amountFractionalBase,
+ Amounts,
+ Balance,
+ BalancesResponse,
+ i18n,
} from "@gnu-taler/taler-util";
-import { JSX } from "preact";
+import { h, VNode } 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 function BalancePage({
+ goToWalletManualWithdraw,
+}: {
+ goToWalletManualWithdraw: () => void;
+}): VNode {
+ const balance = useBalances();
+ return (
+ <BalanceView
+ balance={balance}
+ Linker={PageLink}
+ goToWalletManualWithdraw={goToWalletManualWithdraw}
+ />
+ );
}
export interface BalanceViewProps {
@@ -36,9 +47,13 @@ export interface BalanceViewProps {
goToWalletManualWithdraw: () => void;
}
-export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) {
+export function BalanceView({
+ balance,
+ Linker,
+ goToWalletManualWithdraw,
+}: BalanceViewProps): VNode {
if (!balance) {
- return <span />
+ return <span />;
}
if (balance.hasError) {
@@ -50,38 +65,45 @@ export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: Balan
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>
- )
+ <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}
- />
+ return (
+ <ShowBalances
+ wallet={balance.response}
+ onWithdraw={goToWalletManualWithdraw}
+ />
+ );
}
-function formatPending(entry: Balance): JSX.Element {
- let incoming: JSX.Element | undefined;
- let payment: JSX.Element | undefined;
+function formatPending(entry: Balance): VNode {
+ let incoming: VNode | undefined;
+ let payment: VNode | undefined;
- const available = Amounts.parseOrThrow(entry.available);
+ // const available = Amounts.parseOrThrow(entry.available);
const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
- const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
+ // 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>
+ <span>
+ <i18n.Translate>
+ <span style={{ color: "darkgreen" }}>
+ {"+"}
+ {renderAmount(entry.pendingIncoming)}
+ </span>{" "}
+ incoming
+ </i18n.Translate>
+ </span>
);
}
@@ -100,27 +122,36 @@ function formatPending(entry: Balance): JSX.Element {
);
}
-
-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>
+function ShowBalances({
+ wallet,
+ onWithdraw,
+}: {
+ wallet: BalancesResponse;
+ onWithdraw: () => void;
+}): VNode {
+ 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
index 35da52392..300e9cd57 100644
--- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.stories.tsx
@@ -15,42 +15,40 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { CreateManualWithdraw as TestedComponent } from './CreateManualWithdraw';
+import { createExample } from "../test-utils";
+import { CreateManualWithdraw as TestedComponent } from "./CreateManualWithdraw";
export default {
- title: 'wallet/manual withdraw/creation',
+ title: "wallet/manual withdraw/creation",
component: TestedComponent,
- argTypes: {
- }
+ argTypes: {},
};
+// ,
+const exchangeList = {
+ "http://exchange.taler:8081": "COL",
+ "http://exchange.tal": "EUR",
+};
export const InitialState = createExample(TestedComponent, {
+ exchangeList,
});
-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 WithAmountInitialized = createExample(TestedComponent, {
+ initialAmount: "10",
+ exchangeList,
});
export const WithExchangeError = createExample(TestedComponent, {
- initialExchange: 'http://exchange.tal',
- error: 'The exchange url seems invalid'
+ error: "The exchange url seems invalid",
+ exchangeList,
});
export const WithAmountError = createExample(TestedComponent, {
- currency: 'COL',
- initialExchange: 'http://exchange.taler:8081',
- initialAmount: 'e'
+ initialAmount: "e",
+ exchangeList,
});
diff --git a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
index be2cbe41d..140ac2d40 100644
--- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
@@ -1,56 +1,149 @@
+/*
+ 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 { AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { VNode } from "preact";
-import { useEffect, useRef, useState } from "preact/hooks";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
import { ErrorMessage } from "../components/ErrorMessage";
-import { ButtonPrimary, Input, InputWithLabel, LightText, WalletBox } from "../components/styled";
+import { SelectList } from "../components/SelectList";
+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;
+ exchangeList: Record<string, string>;
onCreate: (exchangeBaseUrl: string, amount: AmountJson) => Promise<void>;
}
-export function CreateManualWithdraw({ onExchangeChange, initialExchange, initialAmount, error, currency, onCreate }: Props): VNode {
+export function CreateManualWithdraw({
+ initialAmount,
+ exchangeList,
+ error,
+ onCreate,
+}: Props): VNode {
+ const exchangeSelectList = Object.keys(exchangeList);
+ const currencySelectList = Object.values(exchangeList);
+ const exchangeMap = exchangeSelectList.reduce(
+ (p, c) => ({ ...p, [c]: `${c} (${exchangeList[c]})` }),
+ {} as Record<string, string>,
+ );
+ const currencyMap = currencySelectList.reduce(
+ (p, c) => ({ ...p, [c]: c }),
+ {} as Record<string, string>,
+ );
+
+ const initialExchange =
+ exchangeSelectList.length > 0 ? exchangeSelectList[0] : "";
+
const [exchange, setExchange] = useState(initialExchange || "");
+ const [currency, setCurrency] = useState(exchangeList[initialExchange] ?? "");
+
const [amount, setAmount] = useState(initialAmount || "");
- const parsedAmount = Amounts.parse(`${currency}:${amount}`)
+ 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])
+ function changeExchange(exchange: string): void {
+ setExchange(exchange);
+ setCurrency(exchangeList[exchange]);
+ }
+ function changeCurrency(currency: string): void {
+ setCurrency(currency);
+ const found = Object.entries(exchangeList).find((e) => e[1] === currency);
+
+ if (found) {
+ setExchange(found[0]);
+ } else {
+ setExchange("");
+ }
+ }
+
+ if (!initialExchange) {
+ return <div>There is no known exchange where to withdraw, add one</div>;
+ }
return (
<WalletBox>
<section>
- <ErrorMessage title={error && "Can't create the reserve"} description={error} />
+ <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>
+ <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>
+ <SelectList
+ label="Currency"
+ list={currencyMap}
+ name="currency"
+ value={currency}
+ onChange={changeCurrency}
+ />
+ </Input>
+ <Input>
+ <SelectList
+ label="Exchange"
+ list={exchangeMap}
+ name="currency"
+ value={exchange}
+ onChange={changeExchange}
+ />
</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 style={{ display: "flex", justifyContent: "right" }}>
+ <a href="" style={{ marginLeft: "auto" }}>
+ Add new exchange
+ </a>
+ </p> */}
+ {currency && (
+ <InputWithLabel invalid={!!amount && !parsedAmount}>
+ <label>Amount</label>
+ <div>
+ <span>{currency}</span>
+ <input
+ type="number"
+ value={amount}
+ onInput={(e) => setAmount(e.currentTarget.value)}
+ />
+ </div>
+ </InputWithLabel>
+ )}
</p>
</section>
<footer>
<div />
- <ButtonPrimary disabled={!parsedAmount || !exchange} onClick={() => onCreate(exchange, parsedAmount!)}>Create</ButtonPrimary>
+ <ButtonPrimary
+ disabled={!parsedAmount || !exchange}
+ onClick={() => onCreate(exchange, parsedAmount!)}
+ >
+ Start withdrawal
+ </ButtonPrimary>
</footer>
</WalletBox>
);
diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
index 0ac4be9a6..9ae3ac3bd 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx
@@ -15,133 +15,146 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import {
PaymentStatus,
- TransactionCommon, TransactionDeposit, TransactionPayment,
- TransactionRefresh, TransactionRefund, TransactionTip, TransactionType,
+ TransactionCommon,
+ TransactionDeposit,
+ TransactionPayment,
+ TransactionRefresh,
+ TransactionRefund,
+ TransactionTip,
+ 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";
+import { createExample } from "../test-utils";
export default {
- title: 'wallet/history/list',
+ title: "wallet/history/list",
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 = () =>
+ ({
+ amountRaw: "USD:10",
+ amountEffective: "USD:9",
+ pending: false,
+ timestamp: {
+ t_ms: new Date().getTime() - count++ * 1000 * 60 * 60 * 7,
+ },
+ transactionId: "12",
+ } as TransactionCommon);
const exampleData = {
withdraw: {
...commonTransaction(),
type: TransactionType.Withdrawal,
- exchangeBaseUrl: 'http://exchange.demo.taler.net',
+ exchangeBaseUrl: "http://exchange.demo.taler.net",
withdrawalDetails: {
confirmed: false,
- exchangePaytoUris: ['payto://x-taler-bank/bank/account'],
+ exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
type: WithdrawalType.ManualTransfer,
- }
+ },
} as TransactionWithdrawal,
payment: {
...commonTransaction(),
- amountEffective: 'USD:11',
+ amountEffective: "USD:11",
type: TransactionType.Payment,
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',
+ proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
} 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',
+ exchangeBaseUrl: "http://exchange.taler",
} as TransactionRefresh,
tip: {
...commonTransaction(),
type: TransactionType.Tip,
- merchantBaseUrl: 'http://ads.merchant.taler.net/',
+ merchantBaseUrl: "http://ads.merchant.taler.net/",
} as TransactionTip,
refund: {
...commonTransaction(),
type: TransactionType.Refund,
- refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
+ refundedTransactionId:
+ "payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
info: {
- contractTermsHash: 'ASDZXCASD',
+ contractTermsHash: "ASDZXCASD",
merchant: {
- name: 'the merchant',
+ name: "the merchant",
},
- orderId: '2021.167-03NPY6MCYMVGT',
+ orderId: "2021.167-03NPY6MCYMVGT",
products: [],
- summary: 'the summary',
- fulfillmentMessage: '',
+ summary: "the summary",
+ fulfillmentMessage: "",
},
} as TransactionRefund,
-}
+};
export const Empty = createExample(TestedComponent, {
list: [],
- balances: [{
- available: 'TESTKUDOS:10',
- pendingIncoming: 'TESTKUDOS:0',
- pendingOutgoing: 'TESTKUDOS:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
+ balances: [
+ {
+ available: "TESTKUDOS:10",
+ pendingIncoming: "TESTKUDOS:0",
+ pendingOutgoing: "TESTKUDOS:0",
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ },
+ ],
});
-
export const One = createExample(TestedComponent, {
list: [exampleData.withdraw],
- balances: [{
- available: 'USD:10',
- pendingIncoming: 'USD:0',
- pendingOutgoing: 'USD:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
+ 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,
- }]
+ list: [
+ {
+ ...exampleData.withdraw,
+ pending: true,
+ },
+ ],
+ balances: [
+ {
+ available: "USD:10",
+ pendingIncoming: "USD:0",
+ pendingOutgoing: "USD:0",
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ },
+ ],
});
export const Several = createExample(TestedComponent, {
@@ -154,20 +167,23 @@ export const Several = createExample(TestedComponent, {
...exampleData.payment,
info: {
...exampleData.payment.info,
- summary: 'this is a long summary that may be cropped because its too long',
+ summary:
+ "this is a long summary that may be cropped because its too long",
},
},
exampleData.refund,
exampleData.tip,
exampleData.deposit,
],
- balances: [{
- available: 'TESTKUDOS:10',
- pendingIncoming: 'TESTKUDOS:0',
- pendingOutgoing: 'TESTKUDOS:0',
- hasPendingTransactions: false,
- requiresUserInput: false,
- }]
+ balances: [
+ {
+ available: "TESTKUDOS:10",
+ pendingIncoming: "TESTKUDOS:0",
+ pendingOutgoing: "TESTKUDOS:0",
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ },
+ ],
});
export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
@@ -181,18 +197,20 @@ export const SeveralWithTwoCurrencies = createExample(TestedComponent, {
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,
- }]
+ 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/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx
index 8160f8574..6b1a21852 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.tsx
@@ -14,22 +14,28 @@
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 {
+ AmountString,
+ Balance,
+ Transaction,
+ TransactionsResponse,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { DateSeparator, WalletBox } from "../components/styled";
+import { Time } from "../components/Time";
import { TransactionItem } from "../components/TransactionItem";
import { useBalances } from "../hooks/useBalances";
import * as wxApi from "../wxApi";
-
-export function HistoryPage(props: any): JSX.Element {
+export function HistoryPage(): VNode {
const [transactions, setTransactions] = useState<
TransactionsResponse | undefined
>(undefined);
- const balance = useBalances()
- const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || [])
+ const balance = useBalances();
+ const balanceWithoutError = balance?.hasError
+ ? []
+ : balance?.response.balances || [];
useEffect(() => {
const fetchData = async (): Promise<void> => {
@@ -43,45 +49,85 @@ export function HistoryPage(props: any): JSX.Element {
return <div>Loading ...</div>;
}
- return <HistoryView balances={balanceWithoutError} list={[...transactions.transactions].reverse()} />;
+ 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)}`
+function amountToString(c: AmountString): string {
+ const idx = c.indexOf(":");
+ return `${c.substring(idx + 1)} ${c.substring(0, idx)}`;
}
+const term = 1000 * 60 * 60 * 24;
+function normalizeToDay(x: number): number {
+ return Math.round(x / term) * term;
+}
+export function HistoryView({
+ list,
+ balances,
+}: {
+ list: Transaction[];
+ balances: Balance[];
+}): VNode {
+ const byDate = list.reduce((rv, x) => {
+ const theDate =
+ x.timestamp.t_ms === "never" ? 0 : normalizeToDay(x.timestamp.t_ms);
+ if (theDate) {
+ (rv[theDate] = rv[theDate] || []).push(x);
+ }
-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
+ 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 (
+ <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, i) => (
+ <li key={i}>{b.available}</li>
+ ))}
+ </ul>
+ </div>
+ )}
+ </header>
+ )}
+ <section>
+ {Object.keys(byDate).map((d, i) => {
+ return (
+ <Fragment key={i}>
+ <DateSeparator>
+ <Time
+ timestamp={{ t_ms: Number.parseInt(d, 10) }}
+ format="dd MMMM yyyy"
+ />
+ </DateSeparator>
+ {byDate[d].map((tx, i) => (
+ <TransactionItem
+ key={i}
+ tx={tx}
+ multiCurrency={multiCurrency}
+ />
+ ))}
+ </Fragment>
+ );
+ })}
+ </section>
+ </WalletBox>
+ );
}
diff --git a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx
index dcc0002e6..1af4e8d8d 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx
@@ -14,68 +14,83 @@
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 { VNode, h } from "preact";
+import { useState } from "preact/hooks";
import { CreateManualWithdraw } from "./CreateManualWithdraw";
-import * as wxApi from '../wxApi'
-import { AcceptManualWithdrawalResult, AmountJson, Amounts } from "@gnu-taler/taler-util";
+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 { route } from "preact-router";
import { Pages } from "../NavigationBar.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
-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)
+export function ManualWithdrawPage(): VNode {
+ const [success, setSuccess] = useState<
+ | {
+ response: AcceptManualWithdrawalResult;
+ exchangeBaseUrl: string;
+ amount: AmountJson;
}
- } catch (e) {
- setError('The exchange url seems invalid')
- setCurrency(undefined)
- }
- }
+ | undefined
+ >(undefined);
+ const [error, setError] = useState<string | undefined>(undefined);
+
+ const knownExchangesHook = useAsyncAsHook(() => wxApi.listExchanges());
- async function doCreate(exchangeBaseUrl: string, amount: AmountJson) {
+ async function doCreate(
+ exchangeBaseUrl: string,
+ amount: AmountJson,
+ ): Promise<void> {
try {
- const resp = await wxApi.acceptManualWithdrawal(exchangeBaseUrl, Amounts.stringify(amount))
- setSuccess(resp)
+ const response = await wxApi.acceptManualWithdrawal(
+ exchangeBaseUrl,
+ Amounts.stringify(amount),
+ );
+ setSuccess({ exchangeBaseUrl, response, amount });
} catch (e) {
if (e instanceof Error) {
- setError(e.message)
+ setError(e.message);
} else {
- setError('unexpected error')
+ setError("unexpected error");
}
- setSuccess(undefined)
+ setSuccess(undefined);
}
}
if (success) {
- return <ReserveCreated reservePub={success.reservePub} paytos={success.exchangePaytoUris} onBack={() => {
- route(Pages.balance)
- }}/>
+ return (
+ <ReserveCreated
+ reservePub={success.response.reservePub}
+ payto={success.response.exchangePaytoUris[0]}
+ exchangeBaseUrl={success.exchangeBaseUrl}
+ amount={success.amount}
+ onBack={() => {
+ route(Pages.balance);
+ }}
+ />
+ );
}
- return <CreateManualWithdraw
- error={error} currency={currency}
- onCreate={doCreate} onExchangeChange={onExchangeChange}
- />;
-}
-
-
+ if (!knownExchangesHook || knownExchangesHook.hasError) {
+ return <div>No Known exchanges</div>;
+ }
+ const exchangeList = knownExchangesHook.response.exchanges.reduce(
+ (p, c) => ({
+ ...p,
+ [c.exchangeBaseUrl]: c.currency,
+ }),
+ {} as Record<string, string>,
+ );
+ return (
+ <CreateManualWithdraw
+ error={error}
+ exchangeList={exchangeList}
+ onCreate={doCreate}
+ />
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
index d1e76c053..5c4e56b15 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx
@@ -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 { createExample } from "../test-utils";
+import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage";
export default {
- title: 'wallet/backup/confirm',
+ title: "wallet/backup/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/',
+ 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/',
+ 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/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
index 4890e5e9c..75292b7e4 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx
@@ -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 { createExample } from "../test-utils";
+import { SetUrlView as TestedComponent } from "./ProviderAddPage";
export default {
- title: 'wallet/backup/add',
+ title: "wallet/backup/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 = createExample(TestedComponent, {
-});
+export const Initial = createExample(TestedComponent, {});
export const WithValue = createExample(TestedComponent, {
- initialValue: 'sync.demo.taler.net'
-});
+ initialValue: "sync.demo.taler.net",
+});
export const WithConnectionError = createExample(TestedComponent, {
- withError: 'Network error'
-});
+ withError: "Network error",
+});
export const WithClientError = createExample(TestedComponent, {
- withError: 'URL may not be right: (404) Not Found'
-});
+ withError: "URL may not be right: (404) Not Found",
+});
export const WithServerError = createExample(TestedComponent, {
- withError: 'Try another server: (500) Internal Server Error'
-});
+ 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..a170620a3 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx
@@ -15,224 +15,221 @@
*/
/**
-*
-* @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 { ProviderPaymentType } from "@gnu-taler/taler-wallet-core";
+import { createExample } from "../test-utils";
+import { ProviderView as TestedComponent } from "./ProviderDetailPage";
export default {
- title: 'wallet/backup/details',
+ title: "wallet/backup/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, {
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: {
+ t_ms: 1625063925078,
+ },
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
+ 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
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp: {
+ t_ms: 1625063925078,
},
lastAttemptedBackupTimestamp: {
- "t_ms": 1625063925078
+ t_ms: 1625063925078,
},
- "paymentProposalIds": [
- "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG"
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
+ 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"
- }
- }
+ 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"
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp: {
+ t_ms: 1625063925078,
+ },
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: {
+ t_ms: 1656599921000,
+ },
},
backupProblem: {
- type: 'backup-unreadable'
- },
- "terms": {
- "annualFee": "EUR:1",
- "storageLimitInMegabytes": 16,
- "supportedProtocolVersion": "0.0"
- }
- }
+ 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"
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.taler:9967/",
+ lastSuccessfulBackupTimestamp: {
+ t_ms: 1625063925078,
+ },
+ paymentProposalIds: [
+ "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG",
],
- "paymentStatus": {
- "type": ProviderPaymentType.Paid,
- "paidUntil": {
- "t_ms": 1656599921000
- }
+ paymentStatus: {
+ type: ProviderPaymentType.Paid,
+ paidUntil: {
+ t_ms: 1656599921000,
+ },
},
backupProblem: {
- type: 'backup-conflicting-device',
- myDeviceId: 'my-device-id',
- otherDeviceId: 'other-device-id',
+ 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"
- }
- }
+ 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"
- }
- }
+ 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"
- }
- }
+ 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"
- }
- }
+ 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,
+ active: true,
+ name: "sync.demo",
+ syncProviderBaseUrl: "http://sync.demo.taler.net/",
+ paymentProposalIds: [],
+ paymentStatus: {
+ type: ProviderPaymentType.TermsChanged,
paidUntil: {
- t_ms: 1656599921000
+ t_ms: 1656599921000,
},
newTerms: {
- "annualFee": "EUR:10",
- "storageLimitInMegabytes": 8,
- "supportedProtocolVersion": "0.0"
+ 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"
- }
- }
+ 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/wallet/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
index c45458eb7..1c14c6e0a 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx
@@ -14,13 +14,23 @@
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 { i18n } from "@gnu-taler/taler-util";
+import {
+ ProviderInfo,
+ ProviderPaymentStatus,
+ ProviderPaymentType,
+} from "@gnu-taler/taler-wallet-core";
+import { Fragment, h, VNode } from "preact";
import { ErrorMessage } from "../components/ErrorMessage";
-import { Button, ButtonDestructive, ButtonPrimary, PaymentStatus, WalletBox, SmallLightText } from "../components/styled";
+import {
+ Button,
+ ButtonDestructive,
+ ButtonPrimary,
+ PaymentStatus,
+ SmallLightText,
+ WalletBox,
+} from "../components/styled";
+import { Time } from "../components/Time";
import { useProviderStatus } from "../hooks/useProviderStatus";
interface Props {
@@ -29,20 +39,29 @@ interface Props {
}
export function ProviderDetailPage({ pid, onBack }: Props): VNode {
- const status = useProviderStatus(pid)
+ const status = useProviderStatus(pid);
if (!status) {
- return <div><i18n.Translate>Loading...</i18n.Translate></div>
+ return (
+ <div>
+ <i18n.Translate>Loading...</i18n.Translate>
+ </div>
+ );
}
if (!status.info) {
- onBack()
- return <div />
+ onBack();
+ return <div />;
}
- return <ProviderView info={status.info}
- onSync={status.sync}
- onDelete={() => status.remove().then(onBack)}
- onBack={onBack}
- onExtend={() => { null }}
- />;
+ return (
+ <ProviderView
+ info={status.info}
+ onSync={status.sync}
+ onDelete={() => status.remove().then(onBack)}
+ onBack={onBack}
+ onExtend={() => {
+ null;
+ }}
+ />
+ );
}
export interface ViewProps {
@@ -53,143 +72,204 @@ export interface ViewProps {
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
+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 (
<WalletBox>
<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>
+ <b>Last backup:</b> <Time timestamp={lb} format="dd MMMM 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>}
+ <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>&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 onClick={onBack}>
+ <i18n.Translate> &lt; back</i18n.Translate>
+ </Button>
<div>
- <ButtonDestructive onClick={onDelete}><i18n.Translate>remove provider</i18n.Translate></ButtonDestructive>
+ <ButtonDestructive onClick={onDelete}>
+ <i18n.Translate>remove provider</i18n.Translate>
+ </ButtonDestructive>
</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`
-}
+// function daysSince(d?: Timestamp): string {
+// 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 }) {
+function Error({ info }: { info: ProviderInfo }): VNode {
if (info.lastError) {
- return <ErrorMessage title={info.lastError.hint} />
+ 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>} />
+ 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" />
+ 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 (
+ <ErrorMessage
+ title={
+ <Fragment>
+ <i18n.Translate>
+ Unknown backup problem: {JSON.stringify(info.backupProblem)}
+ </i18n.Translate>
+ </Fragment>
+ }
+ />
+ );
}
}
- 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 colorByStatus(status: ProviderPaymentType): string {
+// 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): 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.str`service paid`}</span>;
}
+ return (
+ <Fragment>
+ <b>Backup valid until:</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/ReserveCreated.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
index ca524f4e2..8d7b65b3c 100644
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
@@ -15,26 +15,39 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { ReserveCreated as TestedComponent } from './ReserveCreated';
+import { createExample } from "../test-utils";
+import { ReserveCreated as TestedComponent } from "./ReserveCreated";
export default {
- title: 'wallet/manual withdraw/reserve created',
+ title: "wallet/manual withdraw/reserve created",
component: TestedComponent,
- argTypes: {
- }
+ 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',
- ]
+export const TalerBank = createExample(TestedComponent, {
+ reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+ payto:
+ "payto://x-taler-bank/bank.taler:5882/exchangeminator?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+ amount: {
+ currency: "USD",
+ value: 10,
+ fraction: 0,
+ },
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
});
+export const IBAN = createExample(TestedComponent, {
+ reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+ payto:
+ "payto://iban/ASDQWEASDZXCASDQWE?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+ amount: {
+ currency: "USD",
+ value: 10,
+ fraction: 0,
+ },
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
index e01336e02..a72026ab8 100644
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
@@ -1,40 +1,155 @@
-import { Fragment, VNode } from "preact";
-import { useState } from "preact/hooks";
+import {
+ AmountJson,
+ Amounts,
+ parsePaytoUri,
+ PaytoUri,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
import { QR } from "../components/QR";
-import { ButtonBox, FontIcon, WalletBox } from "../components/styled";
-
+import {
+ ButtonDestructive,
+ ButtonPrimary,
+ WalletBox,
+ WarningBox,
+} from "../components/styled";
export interface Props {
reservePub: string;
- paytos: string[];
+ payto: string;
+ exchangeBaseUrl: string;
+ amount: AmountJson;
onBack: () => void;
}
-export function ReserveCreated({ reservePub, paytos, onBack }: Props): VNode {
- const [opened, setOpened] = useState(-1)
+interface BankDetailsProps {
+ payto: PaytoUri;
+ exchangeBaseUrl: string;
+ subject: string;
+ amount: string;
+}
+
+function Row({
+ name,
+ value,
+ literal,
+}: {
+ name: string;
+ value: string;
+ literal?: boolean;
+}): VNode {
+ const [copied, setCopied] = useState(false);
+ function copyText(): void {
+ navigator.clipboard.writeText(value);
+ setCopied(true);
+ }
+ useEffect(() => {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }, [copied]);
+ return (
+ <tr>
+ <td>
+ {!copied ? (
+ <ButtonPrimary small onClick={copyText}>
+ &nbsp; Copy &nbsp;
+ </ButtonPrimary>
+ ) : (
+ <ButtonPrimary small disabled>
+ Copied
+ </ButtonPrimary>
+ )}
+ </td>
+ <td>
+ <b>{name}</b>
+ </td>
+ {literal ? (
+ <td>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {value}
+ </pre>
+ </td>
+ ) : (
+ <td>{value}</td>
+ )}
+ </tr>
+ );
+}
+
+function BankDetailsByPaytoType({
+ payto,
+ subject,
+ exchangeBaseUrl,
+ amount,
+}: BankDetailsProps): VNode {
+ const firstPart = !payto.isKnown ? (
+ <Fragment>
+ <Row name="Account" value={payto.targetPath} />
+ <Row name="Exchange" value={exchangeBaseUrl} />
+ </Fragment>
+ ) : payto.targetType === "x-taler-bank" ? (
+ <Fragment>
+ <Row name="Bank host" value={payto.host} />
+ <Row name="Bank account" value={payto.account} />
+ <Row name="Exchange" value={exchangeBaseUrl} />
+ </Fragment>
+ ) : payto.targetType === "iban" ? (
+ <Fragment>
+ <Row name="IBAN" value={payto.iban} />
+ <Row name="Exchange" value={exchangeBaseUrl} />
+ </Fragment>
+ ) : undefined;
+ return (
+ <table>
+ {firstPart}
+ <Row name="Amount" value={amount} />
+ <Row name="Subject" value={subject} literal />
+ </table>
+ );
+}
+export function ReserveCreated({
+ reservePub,
+ payto,
+ onBack,
+ exchangeBaseUrl,
+ amount,
+}: Props): VNode {
+ const paytoURI = parsePaytoUri(payto);
+ // const url = new URL(paytoURI?.targetPath);
+ if (!paytoURI) {
+ return <div>could not parse payto uri from exchange {payto}</div>;
+ }
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>
+ <h1>Bank transfer details</h1>
+ <p>
+ Please wire <b>{Amounts.stringify(amount)}</b> to:
+ </p>
+ <BankDetailsByPaytoType
+ amount={Amounts.stringify(amount)}
+ exchangeBaseUrl={exchangeBaseUrl}
+ payto={paytoURI}
+ subject={reservePub}
+ />
</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>
+ <p>
+ <WarningBox>
+ Make sure to use the correct subject, otherwise the money will not
+ arrive in this wallet.
+ </WarningBox>
+ </p>
+ <p>
+ Alternative, you can also scan this QR code or open{" "}
+ <a href={payto}>this link</a> if you have a banking app installed that
+ supports RFC 8905
+ </p>
+ <QR text={payto} />
</section>
<footer>
- <ButtonBox onClick={onBack}><FontIcon>&#x2190;</FontIcon></ButtonBox>
<div />
+ <ButtonDestructive onClick={onBack}>Cancel withdraw</ButtonDestructive>
</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..6cc1368d5 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx
@@ -15,39 +15,41 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
-import { createExample } from '../test-utils';
-import { SettingsView as TestedComponent } from './Settings';
+import { createExample } from "../test-utils";
+import { SettingsView as TestedComponent } from "./Settings";
export default {
- title: 'wallet/settings',
+ title: "wallet/settings",
component: TestedComponent,
argTypes: {
setDeviceName: () => Promise.resolve(),
- }
+ },
};
export const AllOff = createExample(TestedComponent, {
- deviceName: 'this-is-the-device-name',
+ deviceName: "this-is-the-device-name",
setDeviceName: () => Promise.resolve(),
});
export const OneChecked = createExample(TestedComponent, {
- deviceName: 'this-is-the-device-name',
+ deviceName: "this-is-the-device-name",
permissionsEnabled: true,
setDeviceName: () => Promise.resolve(),
});
export const WithOneExchange = createExample(TestedComponent, {
- deviceName: 'this-is-the-device-name',
+ deviceName: "this-is-the-device-name",
permissionsEnabled: true,
setDeviceName: () => Promise.resolve(),
- knownExchanges: [{
- currency: 'USD',
- exchangeBaseUrl: 'http://exchange.taler',
- paytoUris: ['payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator']
- }]
+ knownExchanges: [
+ {
+ currency: "USD",
+ exchangeBaseUrl: "http://exchange.taler",
+ paytoUris: ["payto://x-taler-bank/bank.rpi.sebasjm.com/exchangeminator"],
+ },
+ ],
});
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
index 8d18586b1..8d8f3cdbc 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
@@ -14,7 +14,6 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-
import { ExchangeListItem, i18n } from "@gnu-taler/taler-util";
import { VNode, h, Fragment } from "preact";
import { Checkbox } from "../components/Checkbox";
@@ -30,18 +29,28 @@ import * as wxApi from "../wxApi";
export function SettingsPage(): VNode {
const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
- const { devMode, toggleDevMode } = useDevContext()
- const { name, update } = useBackupDeviceName()
- const [lang, changeLang] = useLang()
+ const { devMode, toggleDevMode } = useDevContext();
+ const { name, update } = useBackupDeviceName();
+ const [lang, changeLang] = useLang();
const exchangesHook = useAsyncAsHook(() => wxApi.listExchanges());
- 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}
- />;
+ 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}
+ />
+ );
}
export interface ViewProps {
@@ -56,52 +65,72 @@ export interface ViewProps {
knownExchanges: Array<ExchangeListItem>;
}
-import { strings as messages } from '../i18n/strings'
+import { strings as messages } from "../i18n/strings";
type LangsNames = {
- [P in keyof typeof messages]: string
-}
+ [P in keyof typeof messages]: string;
+};
const names: LangsNames = {
- es: 'Español [es]',
- en: 'English [en]',
- fr: 'Français [fr]',
- de: 'Deutsch [de]',
- sv: 'Svenska [sv]',
- it: 'Italiano [it]',
-}
+ es: "Español [es]",
+ en: "English [en]",
+ fr: "Français [fr]",
+ de: "Deutsch [de]",
+ sv: "Svenska [sv]",
+ it: "Italiano [it]",
+};
-
-export function SettingsView({ knownExchanges, lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode {
+export function SettingsView({
+ knownExchanges,
+ lang,
+ changeLang,
+ deviceName,
+ setDeviceName,
+ permissionsEnabled,
+ togglePermissions,
+ developerMode,
+ toggleDeveloperMode,
+}: ViewProps): VNode {
return (
<WalletBox>
<section>
-
- <h2><i18n.Translate>Known exchanges</i18n.Translate></h2>
- {!knownExchanges || !knownExchanges.length ? <div>
- No exchange yet!
- </div> :
+ <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>)}
+ {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"
+ )}
+
+ <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}
+ enabled={permissionsEnabled}
+ onToggle={togglePermissions}
/>
<h2>Config</h2>
- <Checkbox label="Developer mode"
+ <Checkbox
+ label="Developer mode"
name="devMode"
description="(More options and information useful for debugging)"
- enabled={developerMode} onToggle={toggleDeveloperMode}
+ enabled={developerMode}
+ onToggle={toggleDeveloperMode}
/>
</section>
</WalletBox>
- )
+ );
}
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index 535509cef..c9a3f47cb 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -15,110 +15,116 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
import {
PaymentStatus,
- TransactionCommon, TransactionDeposit, TransactionPayment,
- TransactionRefresh, TransactionRefund, TransactionTip, TransactionType,
+ TransactionCommon,
+ TransactionDeposit,
+ TransactionPayment,
+ TransactionRefresh,
+ TransactionRefund,
+ TransactionTip,
+ TransactionType,
TransactionWithdrawal,
- WithdrawalType
-} from '@gnu-taler/taler-util';
-import { createExample } from '../test-utils';
-import { TransactionView as TestedComponent } from './Transaction';
+ WithdrawalType,
+} from "@gnu-taler/taler-util";
+import { createExample } from "../test-utils";
+import { TransactionView as TestedComponent } from "./Transaction";
export default {
- title: 'wallet/history/details',
+ title: "wallet/history/details",
component: TestedComponent,
argTypes: {
- onRetry: { action: 'onRetry' },
- onDelete: { action: 'onDelete' },
- onBack: { action: 'onBack' },
- }
+ onRetry: { action: "onRetry" },
+ onDelete: { action: "onDelete" },
+ onBack: { action: "onBack" },
+ },
};
const commonTransaction = {
- amountRaw: 'KUDOS:11',
- amountEffective: 'KUDOS:9.2',
+ amountRaw: "KUDOS:11",
+ amountEffective: "KUDOS:9.2",
pending: false,
timestamp: {
- t_ms: new Date().getTime()
+ t_ms: new Date().getTime(),
},
- transactionId: '12',
-} as TransactionCommon
+ transactionId: "12",
+} as TransactionCommon;
const exampleData = {
withdraw: {
...commonTransaction,
type: TransactionType.Withdrawal,
- exchangeBaseUrl: 'http://exchange.taler',
+ exchangeBaseUrl: "http://exchange.taler",
withdrawalDetails: {
confirmed: false,
- exchangePaytoUris: ['payto://x-taler-bank/bank/account'],
+ exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
type: WithdrawalType.ManualTransfer,
- }
+ },
} as TransactionWithdrawal,
payment: {
...commonTransaction,
- amountEffective: 'KUDOS:11',
+ amountEffective: "KUDOS:11",
type: TransactionType.Payment,
info: {
- contractTermsHash: 'ASDZXCASD',
+ contractTermsHash: "ASDZXCASD",
merchant: {
- name: 'the merchant',
+ name: "the merchant",
},
- orderId: '2021.167-03NPY6MCYMVGT',
+ orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
- fulfillmentMessage: '',
+ fulfillmentMessage: "",
},
- proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
+ proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
} 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',
+ exchangeBaseUrl: "http://exchange.taler",
} as TransactionRefresh,
tip: {
...commonTransaction,
type: TransactionType.Tip,
- merchantBaseUrl: 'http://merchant.taler',
+ merchantBaseUrl: "http://merchant.taler",
} as TransactionTip,
refund: {
...commonTransaction,
type: TransactionType.Refund,
- refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0',
+ refundedTransactionId:
+ "payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
info: {
- contractTermsHash: 'ASDZXCASD',
+ contractTermsHash: "ASDZXCASD",
merchant: {
- name: 'the merchant',
+ name: "the merchant",
},
- orderId: '2021.167-03NPY6MCYMVGT',
+ orderId: "2021.167-03NPY6MCYMVGT",
products: [],
- summary: 'the summary',
- fulfillmentMessage: '',
+ summary: "the summary",
+ fulfillmentMessage: "",
},
} as TransactionRefund,
-}
+};
const transactionError = {
code: 2000,
details: "details",
hint: "this is a hint for the error",
- message: 'message'
-}
+ message: "message",
+};
export const Withdraw = createExample(TestedComponent, {
- transaction: exampleData.withdraw
+ transaction: exampleData.withdraw,
});
export const WithdrawError = createExample(TestedComponent, {
@@ -132,24 +138,22 @@ export const WithdrawPending = createExample(TestedComponent, {
transaction: { ...exampleData.withdraw, pending: true },
});
-
export const Payment = createExample(TestedComponent, {
- transaction: exampleData.payment
+ transaction: exampleData.payment,
});
export const PaymentError = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- error: transactionError
+ error: transactionError,
},
});
export const PaymentWithoutFee = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
- amountRaw: 'KUDOS:11',
-
- }
+ amountRaw: "KUDOS:11",
+ },
});
export const PaymentPending = createExample(TestedComponent, {
@@ -161,27 +165,33 @@ export const PaymentWithProducts = createExample(TestedComponent, {
...exampleData.payment,
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,
- }]
- }
+ 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,
});
@@ -190,75 +200,79 @@ export const PaymentWithLongSummary = createExample(TestedComponent, {
...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,
- }]
- }
+ 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
+ transaction: exampleData.deposit,
});
export const DepositError = createExample(TestedComponent, {
transaction: {
...exampleData.deposit,
- error: transactionError
+ error: transactionError,
},
});
export const DepositPending = createExample(TestedComponent, {
- transaction: { ...exampleData.deposit, pending: true }
+ transaction: { ...exampleData.deposit, pending: true },
});
export const Refresh = createExample(TestedComponent, {
- transaction: exampleData.refresh
+ transaction: exampleData.refresh,
});
export const RefreshError = createExample(TestedComponent, {
transaction: {
...exampleData.refresh,
- error: transactionError
+ error: transactionError,
},
});
export const Tip = createExample(TestedComponent, {
- transaction: exampleData.tip
+ transaction: exampleData.tip,
});
export const TipError = createExample(TestedComponent, {
transaction: {
...exampleData.tip,
- error: transactionError
+ error: transactionError,
},
});
export const TipPending = createExample(TestedComponent, {
- transaction: { ...exampleData.tip, pending: true }
+ transaction: { ...exampleData.tip, pending: true },
});
export const Refund = createExample(TestedComponent, {
- transaction: exampleData.refund
+ transaction: exampleData.refund,
});
export const RefundError = createExample(TestedComponent, {
transaction: {
...exampleData.refund,
- error: transactionError
+ error: transactionError,
},
});
export const RefundPending = createExample(TestedComponent, {
- transaction: { ...exampleData.refund, pending: true }
+ transaction: { ...exampleData.refund, pending: true },
});
export const RefundWithProducts = createExample(TestedComponent, {
@@ -266,11 +280,14 @@ export const RefundWithProducts = createExample(TestedComponent, {
...exampleData.refund,
info: {
...exampleData.refund.info,
- products: [{
- description: 't-shirt',
- }, {
- description: 'beer',
- }]
- }
+ products: [
+ {
+ description: "t-shirt",
+ },
+ {
+ description: "beer",
+ },
+ ],
+ },
} as TransactionRefund,
});
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index cf41efb59..1472efb40 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -14,27 +14,42 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountLike, Amounts, i18n, Transaction, TransactionType } from "@gnu-taler/taler-util";
-import { format } from "date-fns";
-import { JSX, VNode } from "preact";
-import { route } from 'preact-router';
+import {
+ AmountLike,
+ Amounts,
+ i18n,
+ Transaction,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { route } from "preact-router";
import { useEffect, useState } from "preact/hooks";
import emptyImg from "../../static/img/empty.png";
import { ErrorMessage } from "../components/ErrorMessage";
import { Part } from "../components/Part";
-import { ButtonBox, ButtonBoxDestructive, ButtonPrimary, FontIcon, ListOfProducts, RowBorderGray, SmallLightText, WalletBox, WarningBox } from "../components/styled";
+import {
+ Button,
+ ButtonDestructive,
+ ButtonPrimary,
+ ListOfProducts,
+ RowBorderGray,
+ SmallLightText,
+ WalletBox,
+ WarningBox,
+} from "../components/styled";
+import { Time } from "../components/Time";
import { Pages } from "../NavigationBar";
import * as wxApi from "../wxApi";
-export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
- const [transaction, setTransaction] = useState<
- Transaction | undefined
- >(undefined);
+export function TransactionPage({ tid }: { tid: string }): VNode {
+ 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);
+ const ts = res.transactions.filter((t) => t.transactionId === tid);
if (ts.length === 1) {
setTransaction(ts[0]);
} else {
@@ -45,13 +60,22 @@ export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
}, [tid]);
if (!transaction) {
- return <div><i18n.Translate>Loading ...</i18n.Translate></div>;
+ return (
+ <div>
+ <i18n.Translate>Loading ...</i18n.Translate>
+ </div>
+ );
}
- return <TransactionView
- transaction={transaction}
- onDelete={() => wxApi.deleteTransaction(tid).then(_ => history.go(-1))}
- onRetry={() => wxApi.retryTransaction(tid).then(_ => history.go(-1))}
- onBack={() => { route(Pages.history) }} />;
+ return (
+ <TransactionView
+ transaction={transaction}
+ onDelete={() => wxApi.deleteTransaction(tid).then(() => history.go(-1))}
+ onRetry={() => wxApi.retryTransaction(tid).then(() => history.go(-1))}
+ onBack={() => {
+ route(Pages.history);
+ }}
+ />
+ );
}
export interface WalletTransactionProps {
@@ -61,173 +85,295 @@ export interface WalletTransactionProps {
onBack: () => void;
}
-export function TransactionView({ transaction, onDelete, onRetry, onBack }: WalletTransactionProps) {
-
- 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>
- </section>
- <footer>
- <ButtonBox onClick={onBack}><i18n.Translate> <FontIcon>&#x2190;</FontIcon> </i18n.Translate></ButtonBox>
- <div>
- {transaction?.error ? <ButtonPrimary onClick={onRetry}><i18n.Translate>retry</i18n.Translate></ButtonPrimary> : null}
- <ButtonBoxDestructive onClick={onDelete}><i18n.Translate>&#x1F5D1;</i18n.Translate></ButtonBoxDestructive>
- </div>
- </footer>
- </WalletBox>
+export function TransactionView({
+ transaction,
+ onDelete,
+ onRetry,
+ onBack,
+}: WalletTransactionProps): VNode {
+ function TransactionTemplate({ children }: { children: VNode[] }): VNode {
+ return (
+ <WalletBox>
+ <section style={{ padding: 8, textAlign: "center" }}>
+ <ErrorMessage title={transaction?.error?.hint} />
+ {transaction.pending && (
+ <WarningBox>
+ This transaction is not completed
+ <a href="">more info...</a>
+ </WarningBox>
+ )}
+ </section>
+ <section>
+ <div style={{ textAlign: "center" }}>{children}</div>
+ </section>
+ <footer>
+ <Button onClick={onBack}>
+ <i18n.Translate> &lt; Back </i18n.Translate>
+ </Button>
+ <div>
+ {transaction?.error ? (
+ <ButtonPrimary onClick={onRetry}>
+ <i18n.Translate>retry</i18n.Translate>
+ </ButtonPrimary>
+ ) : null}
+ <ButtonDestructive onClick={onDelete}>
+ <i18n.Translate> Forget </i18n.Translate>
+ </ButtonDestructive>
+ </div>
+ </footer>
+ </WalletBox>
+ );
}
- function amountToString(text: AmountLike) {
- const aj = Amounts.jsonifyAmount(text)
- const amount = Amounts.stringifyValue(aj)
- return `${amount} ${aj.currency}`
+ function amountToString(text: AmountLike): string {
+ const aj = Amounts.jsonifyAmount(text);
+ const amount = Amounts.stringifyValue(aj);
+ return `${amount} ${aj.currency}`;
}
-
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>
+ ).amount;
+ return (
+ <TransactionTemplate>
+ <h2>Withdrawal</h2>
+ <Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
+ <br />
+ <Part
+ big
+ title="Total withdrawn"
+ text={amountToString(transaction.amountEffective)}
+ kind="positive"
+ />
+ <Part
+ big
+ title="Chosen amount"
+ text={amountToString(transaction.amountRaw)}
+ kind="neutral"
+ />
+ <Part
+ big
+ title="Exchange fee"
+ text={amountToString(fee)}
+ kind="negative"
+ />
+ <Part
+ title="Exchange"
+ text={new URL(transaction.exchangeBaseUrl).hostname}
+ kind="neutral"
+ />
+ </TransactionTemplate>
+ );
}
- const showLargePic = () => {
-
- }
+ const showLargePic = (): void => {
+ return;
+ };
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' />
+ ).amount;
- <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>
- <div>
- {p.quantity && p.quantity > 0 && <SmallLightText>x {p.quantity} {p.unit}</SmallLightText>}
- <div>{p.description}</div>
- </div>
- </RowBorderGray>)}
- </ListOfProducts>
- }
- </div>
- </TransactionTemplate>
+ return (
+ <TransactionTemplate>
+ <h2>Payment </h2>
+ <Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
+ <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"
+ />
+
+ <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>
+ <div>
+ {p.quantity && p.quantity > 0 && (
+ <SmallLightText>
+ x {p.quantity} {p.unit}
+ </SmallLightText>
+ )}
+ <div>{p.description}</div>
+ </div>
+ </RowBorderGray>
+ ))}
+ </ListOfProducts>
+ )}
+ </div>
+ </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>
+ ).amount;
+ return (
+ <TransactionTemplate>
+ <h2>Deposit </h2>
+ <Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
+ <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>
+ );
}
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>
+ ).amount;
+ return (
+ <TransactionTemplate>
+ <h2>Refresh</h2>
+ <Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
+ <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>
+ ).amount;
+ return (
+ <TransactionTemplate>
+ <h2>Tip</h2>
+ <Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
+ <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>
+ );
}
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}
- </p>
- <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>
- <div>
- {p.quantity && p.quantity > 0 && <SmallLightText>x {p.quantity} {p.unit}</SmallLightText>}
- <div>{p.description}</div>
- </div>
- </RowBorderGray>)}
- </ListOfProducts>
- }
- </div>
- </TransactionTemplate>
- }
+ ).amount;
+ return (
+ <TransactionTemplate>
+ <h2>Refund</h2>
+ <Time timestamp={transaction.timestamp} format="dd MMMM yyyy, HH:mm" />
+ <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}</p>
+ <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>
+ <div>
+ {p.quantity && p.quantity > 0 && (
+ <SmallLightText>
+ x {p.quantity} {p.unit}
+ </SmallLightText>
+ )}
+ <div>{p.description}</div>
+ </div>
+ </RowBorderGray>
+ ))}
+ </ListOfProducts>
+ )}
+ </div>
+ </TransactionTemplate>
+ );
+ }
- return <div></div>
+ return <div />;
}
diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
index 6579450b3..7e6588fac 100644
--- a/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Welcome.stories.tsx
@@ -15,16 +15,15 @@
*/
/**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
-import { createExample } from '../test-utils';
-import { View as TestedComponent } from './Welcome';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { createExample } from "../test-utils";
+import { View as TestedComponent } from "./Welcome";
export default {
- title: 'wallet/welcome',
+ title: "wallet/welcome",
component: TestedComponent,
};
@@ -32,11 +31,11 @@ export const Normal = createExample(TestedComponent, {
permissionsEnabled: true,
diagnostics: {
errors: [],
- walletManifestVersion: '1.0',
- walletManifestDisplayVersion: '1.0',
+ walletManifestVersion: "1.0",
+ walletManifestDisplayVersion: "1.0",
firefoxIdbProblem: false,
dbOutdated: false,
- }
+ },
});
export const TimedoutDiagnostics = createExample(TestedComponent, {
@@ -47,4 +46,3 @@ export const TimedoutDiagnostics = createExample(TestedComponent, {
export const RunningDiagnostics = createExample(TestedComponent, {
permissionsEnabled: false,
});
-
diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
index d11070d9a..a6dd040e4 100644
--- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
@@ -20,50 +20,61 @@
* @author Florian Dold
*/
-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 { h, VNode } from "preact";
-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 [permissionsEnabled, togglePermissions] = useExtendedPermissions();
+ const [diagnostics, timedOut] = useDiagnostics();
+ return (
+ <View
+ permissionsEnabled={permissionsEnabled}
+ togglePermissions={togglePermissions}
+ diagnostics={diagnostics}
+ timedOut={timedOut}
+ />
+ );
}
export interface ViewProps {
- permissionsEnabled: boolean,
- togglePermissions: () => void,
- diagnostics: WalletDiagnostics | undefined,
- timedOut: boolean,
+ permissionsEnabled: boolean;
+ togglePermissions: () => void;
+ diagnostics: WalletDiagnostics | undefined;
+ timedOut: boolean;
}
-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({
+ permissionsEnabled,
+ togglePermissions,
+ diagnostics,
+ timedOut,
+}: ViewProps): VNode {
+ 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>
);
}
diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
index 023ee94c5..f097d58b5 100644
--- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
@@ -21,7 +21,7 @@
*/
import { setupI18n } from "@gnu-taler/taler-util";
-import { createHashHistory } from 'history';
+import { createHashHistory } from "history";
import { Fragment, h, render } from "preact";
import Router, { route, Route } from "preact-router";
import { useEffect } from "preact/hooks";
@@ -29,22 +29,19 @@ 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 { TipPage } from "./cta/Tip";
import { WithdrawPage } from "./cta/Withdraw";
import { strings } from "./i18n/strings";
-import {
- Pages, WalletNavBar
-} from "./NavigationBar";
+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 { TransactionPage } from "./wallet/Transaction";
import { WelcomePage } from "./wallet/Welcome";
-import { BackupPage } from './wallet/BackupPage';
+import { BackupPage } from "./wallet/BackupPage";
import { DeveloperPage } from "./popup/Debug.js";
import { ManualWithdrawPage } from "./wallet/ManualWithdrawPage.js";
-
function main(): void {
try {
const container = document.getElementById("container");
@@ -69,51 +66,86 @@ if (document.readyState === "loading") {
}
function withLogoAndNavBar(Component: any) {
- return (props: any) => <Fragment>
- <LogoHeader />
- <WalletNavBar />
- <Component {...props} />
- </Fragment>
+ 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>
+ 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
+ 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..90cfd3ed6 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -47,7 +47,12 @@ import {
AddExchangeRequest,
GetExchangeTosResult,
} from "@gnu-taler/taler-util";
-import { AddBackupProviderRequest, BackupProviderState, OperationFailedError, RemoveBackupProviderRequest } from "@gnu-taler/taler-wallet-core";
+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";
@@ -149,78 +154,89 @@ interface CurrencyInfo {
pub: string;
}
interface ListOfKnownCurrencies {
- auditors: CurrencyInfo[],
- exchanges: CurrencyInfo[],
+ 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 }
+ 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", {})
+ return callBackend("listExchanges", {});
}
/**
* Get information about the current state of wallet backups.
*/
export function getBackupInfo(): Promise<BackupInfo> {
- return callBackend("getBackupInfo", {})
+ return callBackend("getBackupInfo", {});
}
/**
* Add a backup provider and activate it
*/
-export function addBackupProvider(backupProviderBaseUrl: string, name: string): Promise<void> {
+export function addBackupProvider(
+ backupProviderBaseUrl: string,
+ name: string,
+): Promise<void> {
return callBackend("addBackupProvider", {
- backupProviderBaseUrl, activate: true, name
- } as AddBackupProviderRequest)
+ backupProviderBaseUrl,
+ activate: true,
+ name,
+ } as AddBackupProviderRequest);
}
export function setWalletDeviceId(walletDeviceId: string): Promise<void> {
return callBackend("setWalletDeviceId", {
- walletDeviceId
- } as SetWalletDeviceIdRequest)
+ walletDeviceId,
+ } as SetWalletDeviceIdRequest);
}
export function syncAllProviders(): Promise<void> {
- return callBackend("runBackupCycle", {})
+ return callBackend("runBackupCycle", {});
}
export function syncOneProvider(url: string): Promise<void> {
- return callBackend("runBackupCycle", { providers: [url] })
+ return callBackend("runBackupCycle", { providers: [url] });
}
export function removeProvider(url: string): Promise<void> {
- return callBackend("removeBackupProvider", { provider: url } as RemoveBackupProviderRequest)
+ return callBackend("removeBackupProvider", {
+ provider: url,
+ } as RemoveBackupProviderRequest);
}
export function extendedProvider(url: string): Promise<void> {
- return callBackend("extendBackupProvider", { provider: url })
+ return callBackend("extendBackupProvider", { provider: url });
}
/**
* Retry a transaction
- * @param transactionId
- * @returns
+ * @param transactionId
+ * @returns
*/
export function retryTransaction(transactionId: string): Promise<void> {
return callBackend("retryTransaction", {
- transactionId
+ transactionId,
} as RetryTransactionRequest);
}
@@ -229,7 +245,7 @@ export function retryTransaction(transactionId: string): Promise<void> {
*/
export function deleteTransaction(transactionId: string): Promise<void> {
return callBackend("deleteTransaction", {
- transactionId
+ transactionId,
} as DeleteTransactionRequest);
}
@@ -264,29 +280,30 @@ export function acceptWithdrawal(
/**
* Create a reserve into the exchange that expect the amount indicated
- * @param exchangeBaseUrl
- * @param amount
- * @returns
+ * @param exchangeBaseUrl
+ * @param amount
+ * @returns
*/
export function acceptManualWithdrawal(
exchangeBaseUrl: string,
amount: string,
): Promise<AcceptManualWithdrawalResult> {
return callBackend("acceptManualWithdrawal", {
- amount, exchangeBaseUrl
+ amount,
+ exchangeBaseUrl,
});
}
export function setExchangeTosAccepted(
exchangeBaseUrl: string,
- etag: string | undefined
+ etag: string | undefined,
): Promise<void> {
return callBackend("setExchangeTosAccepted", {
- exchangeBaseUrl, etag
- } as AcceptExchangeTosRequest)
+ exchangeBaseUrl,
+ etag,
+ } as AcceptExchangeTosRequest);
}
-
/**
* Get diagnostics information
*/
@@ -319,7 +336,6 @@ export function getWithdrawalDetailsForUri(
return callBackend("getWithdrawalDetailsForUri", req);
}
-
/**
* Get diagnostics information
*/
@@ -333,17 +349,15 @@ export function getExchangeTos(
acceptedFormat: string[],
): Promise<GetExchangeTosResult> {
return callBackend("getExchangeTos", {
- exchangeBaseUrl, acceptedFormat
+ exchangeBaseUrl,
+ acceptedFormat,
});
}
-export function addExchange(
- req: AddExchangeRequest,
-): Promise<void> {
+export function addExchange(req: AddExchangeRequest): Promise<void> {
return callBackend("addExchange", req);
}
-
export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> {
return callBackend("prepareTip", req);
}
diff --git a/packages/taler-wallet-webextension/static/wallet.html b/packages/taler-wallet-webextension/static/wallet.html
index a1c069d74..f9dd8a19b 100644
--- a/packages/taler-wallet-webextension/static/wallet.html
+++ b/packages/taler-wallet-webextension/static/wallet.html
@@ -2,11 +2,27 @@
<html>
<head>
<meta charset="utf-8" />
- <link rel="stylesheet" type="text/css" href="/static/style/pure.css" />
- <link rel="stylesheet" type="text/css" href="/static/style/wallet.css" />
<link rel="stylesheet" type="text/css" href="/dist/popupEntryPoint.css" />
<link rel="icon" href="/static/img/icon.png" />
<script src="/dist/walletEntryPoint.js"></script>
+ <style>
+ html {
+ font-family: sans-serif; /* 1 */
+ }
+ h1 {
+ font-size: 2em;
+ }
+ input {
+ font: inherit;
+ }
+ body {
+ margin: 0;
+ font-size: 100%;
+ padding: 0;
+ background-color: #f8faf7;
+ font-family: Arial, Helvetica, sans-serif;
+ }
+ </style>
</head>
<body>
diff --git a/packages/taler-wallet-webextension/tsconfig.json b/packages/taler-wallet-webextension/tsconfig.json
index cff3d8857..25920a120 100644
--- a/packages/taler-wallet-webextension/tsconfig.json
+++ b/packages/taler-wallet-webextension/tsconfig.json
@@ -1,9 +1,13 @@
{
"compilerOptions": {
"composite": true,
- "lib": ["es6", "DOM"],
- "jsx": "react-jsx",
- "jsxImportSource": "preact",
+ "lib": [
+ "es6",
+ "DOM"
+ ],
+ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+ "jsxFactory": "h", /* Specify the JSX factory function to use when targeting react JSX emit, e.g. React.createElement or h. */
+ "jsxFragmentFactory": "Fragment", // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#custom-jsx-factories
"moduleResolution": "Node",
"module": "ESNext",
"target": "ES6",
@@ -16,7 +20,9 @@
"esModuleInterop": true,
"importHelpers": true,
"rootDir": "./src",
- "typeRoots": ["./node_modules/@types"]
+ "typeRoots": [
+ "./node_modules/@types"
+ ]
},
"references": [
{
@@ -26,5 +32,7 @@
"path": "../taler-util/"
}
],
- "include": ["src/**/*"]
-}
+ "include": [
+ "src/**/*"
+ ]
+} \ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ce6296f3d..7c3069267 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,10 +7,12 @@ importers:
'@linaria/esbuild': ^3.0.0-beta.13
'@linaria/shaker': ^3.0.0-beta.13
esbuild: ^0.12.29
+ prettier: ^2.4.1
devDependencies:
'@linaria/esbuild': 3.0.0-beta.13
'@linaria/shaker': 3.0.0-beta.13
esbuild: 0.12.29
+ prettier: 2.4.1
packages/anastasis-core:
specifiers:
@@ -62,6 +64,7 @@ importers:
'@typescript-eslint/eslint-plugin': ^5.3.0
'@typescript-eslint/parser': ^5.3.0
anastasis-core: workspace:^0.0.1
+ base64-inline-loader: 1.1.1
bulma: ^0.9.3
bulma-checkbox: ^1.1.1
bulma-radio: ^1.1.1
@@ -86,6 +89,7 @@ importers:
dependencies:
'@gnu-taler/taler-util': link:../taler-util
anastasis-core: link:../anastasis-core
+ base64-inline-loader: 1.1.1
date-fns: 2.25.0
jed: 1.1.1
preact: 10.5.15
@@ -251,7 +255,7 @@ importers:
source-map-support: ^0.5.19
tslib: ^2.1.0
typedoc: ^0.20.16
- typescript: ^4.1.3
+ typescript: ^4.4.4
dependencies:
'@gnu-taler/idb-bridge': link:../idb-bridge
'@gnu-taler/taler-util': link:../taler-util
@@ -265,11 +269,11 @@ importers:
'@ava/typescript': 1.1.1
'@gnu-taler/pogen': link:../pogen
'@microsoft/api-extractor': 7.13.0
- '@typescript-eslint/eslint-plugin': 4.14.0_980e7d90d2d08155204a38366bd3b934
- '@typescript-eslint/parser': 4.14.0_eslint@7.18.0+typescript@4.1.3
+ '@typescript-eslint/eslint-plugin': 4.14.0_4f40ec8f9ae74407a8c29890901bb23f
+ '@typescript-eslint/parser': 4.14.0_eslint@7.18.0+typescript@4.4.4
ava: 3.15.0
eslint: 7.18.0
- eslint-config-airbnb-typescript: 12.0.0_aa91c0ea1e61103ae60b9cd49dfd9775
+ eslint-config-airbnb-typescript: 12.0.0_b55a7168bd2ecdf8767ddb224d20fd7e
eslint-plugin-import: 2.22.1_eslint@7.18.0
eslint-plugin-jsx-a11y: 6.4.1_eslint@7.18.0
eslint-plugin-react: 7.22.0_eslint@7.18.0
@@ -282,8 +286,8 @@ importers:
rollup: 2.59.0
rollup-plugin-sourcemaps: 0.6.3_57eeb328ceff0756ae1d32f4d22d60f9
source-map-resolve: 0.6.0
- typedoc: 0.20.16_typescript@4.1.3
- typescript: 4.1.3
+ typedoc: 0.20.16_typescript@4.4.4
+ typescript: 4.4.4
packages/taler-wallet-embedded:
specifiers:
@@ -494,6 +498,11 @@ packages:
engines: {node: '>=6.9.0'}
dev: true
+ /@babel/compat-data/7.16.4:
+ resolution: {integrity: sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==}
+ engines: {node: '>=6.9.0'}
+ dev: true
+
/@babel/core/7.12.9:
resolution: {integrity: sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==}
engines: {node: '>=6.9.0'}
@@ -713,6 +722,18 @@ packages:
semver: 6.3.0
dev: true
+ /@babel/helper-compilation-targets/7.16.3:
+ resolution: {integrity: sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ dependencies:
+ '@babel/compat-data': 7.16.4
+ '@babel/helper-validator-option': 7.14.5
+ browserslist: 4.18.1
+ semver: 6.3.0
+ dev: true
+
/@babel/helper-create-class-features-plugin/7.15.0_@babel+core@7.13.16:
resolution: {integrity: sha512-MdmDXgvTIi4heDVX/e9EFfeGpugqm9fobBVg/iioE8kueXrOHdRDe36FAY7SnE9xXLVeYCoJR/gdrBEIHRC83Q==}
engines: {node: '>=6.9.0'}
@@ -930,6 +951,23 @@ packages:
- supports-color
dev: true
+ /@babel/helper-define-polyfill-provider/0.3.0:
+ resolution: {integrity: sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg==}
+ peerDependencies:
+ '@babel/core': ^7.4.0-0
+ dependencies:
+ '@babel/helper-compilation-targets': 7.16.3
+ '@babel/helper-module-imports': 7.16.0
+ '@babel/helper-plugin-utils': 7.14.5
+ '@babel/traverse': 7.16.3
+ debug: 4.3.2
+ lodash.debounce: 4.0.8
+ resolve: 1.20.0
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@babel/helper-explode-assignable-expression/7.14.5:
resolution: {integrity: sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ==}
engines: {node: '>=6.9.0'}
@@ -1095,6 +1133,17 @@ packages:
- supports-color
dev: true
+ /@babel/helper-remap-async-to-generator/7.16.4:
+ resolution: {integrity: sha512-vGERmmhR+s7eH5Y/cp8PCVzj4XEjerq8jooMfxFdA5xVtAk9Sh4AQsrWgiErUEBjtGrBtOFKDUcWQFW4/dFwMA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/helper-annotate-as-pure': 7.16.0
+ '@babel/helper-wrap-function': 7.16.0
+ '@babel/types': 7.16.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@babel/helper-replace-supers/7.15.0:
resolution: {integrity: sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA==}
engines: {node: '>=6.9.0'}
@@ -1202,7 +1251,7 @@ packages:
dependencies:
'@babel/helper-function-name': 7.16.0
'@babel/template': 7.16.0
- '@babel/traverse': 7.16.0
+ '@babel/traverse': 7.16.3
'@babel/types': 7.16.0
transitivePeerDependencies:
- supports-color
@@ -1276,6 +1325,12 @@ packages:
hasBin: true
dev: true
+ /@babel/parser/7.16.4:
+ resolution: {integrity: sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+ dev: true
+
/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.2:
resolution: {integrity: sha512-h37CvpLSf8gb2lIJ2CgC3t+EjFbi0t8qS7LCS1xcJIlEXE4czlofwaW7W1HA8zpgOCzI9C1nmoqNR1zWkk0pQg==}
engines: {node: '>=6.9.0'}
@@ -1397,6 +1452,19 @@ packages:
- supports-color
dev: true
+ /@babel/plugin-proposal-async-generator-functions/7.16.4:
+ resolution: {integrity: sha512-/CUekqaAaZCQHleSK/9HajvcD/zdnJiKRiuUFq8ITE+0HsPzquf53cpFiqAwl/UfmJbR6n5uGPQSPdrmKOvHHg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/helper-plugin-utils': 7.14.5
+ '@babel/helper-remap-async-to-generator': 7.16.4
+ '@babel/plugin-syntax-async-generators': 7.8.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@babel/plugin-proposal-class-properties/7.14.5_@babel+core@7.13.16:
resolution: {integrity: sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg==}
engines: {node: '>=6.9.0'}
@@ -1856,11 +1924,11 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
- '@babel/compat-data': 7.16.0
- '@babel/helper-compilation-targets': 7.16.0
+ '@babel/compat-data': 7.16.4
+ '@babel/helper-compilation-targets': 7.16.3
'@babel/helper-plugin-utils': 7.14.5
'@babel/plugin-syntax-object-rest-spread': 7.8.3
- '@babel/plugin-transform-parameters': 7.16.0
+ '@babel/plugin-transform-parameters': 7.16.3
dev: true
/@babel/plugin-proposal-object-rest-spread/7.16.0_@babel+core@7.16.0:
@@ -2860,7 +2928,7 @@ packages:
dependencies:
'@babel/helper-module-imports': 7.16.0
'@babel/helper-plugin-utils': 7.14.5
- '@babel/helper-remap-async-to-generator': 7.16.0
+ '@babel/helper-remap-async-to-generator': 7.16.4
transitivePeerDependencies:
- supports-color
dev: true
@@ -3817,6 +3885,15 @@ packages:
'@babel/helper-plugin-utils': 7.14.5
dev: true
+ /@babel/plugin-transform-parameters/7.16.3:
+ resolution: {integrity: sha512-3MaDpJrOXT1MZ/WCmkOFo7EtmVVC8H4EUZVrHvFOsmwkk4lOjQj8rzv8JKUZV4YoQKeoIgk07GO+acPU9IMu/w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/helper-plugin-utils': 7.14.5
+ dev: true
+
/@babel/plugin-transform-property-literals/7.14.5_@babel+core@7.13.16:
resolution: {integrity: sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw==}
engines: {node: '>=6.9.0'}
@@ -4061,17 +4138,17 @@ packages:
'@babel/helper-plugin-utils': 7.14.5
dev: true
- /@babel/plugin-transform-runtime/7.16.0:
- resolution: {integrity: sha512-zlPf1/XFn5+vWdve3AAhf+Sxl+MVa5VlwTwWgnLx23u4GlatSRQJ3Eoo9vllf0a9il3woQsT4SK+5Z7c06h8ag==}
+ /@babel/plugin-transform-runtime/7.16.4:
+ resolution: {integrity: sha512-pru6+yHANMTukMtEZGC4fs7XPwg35v8sj5CIEmE+gEkFljFiVJxEWxx/7ZDkTK+iZRYo1bFXBtfIN95+K3cJ5A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/helper-module-imports': 7.16.0
'@babel/helper-plugin-utils': 7.14.5
- babel-plugin-polyfill-corejs2: 0.2.3
- babel-plugin-polyfill-corejs3: 0.3.0
- babel-plugin-polyfill-regenerator: 0.2.3
+ babel-plugin-polyfill-corejs2: 0.3.0
+ babel-plugin-polyfill-corejs3: 0.4.0
+ babel-plugin-polyfill-regenerator: 0.3.0
semver: 6.3.0
transitivePeerDependencies:
- supports-color
@@ -4737,6 +4814,90 @@ packages:
- supports-color
dev: true
+ /@babel/preset-env/7.16.4:
+ resolution: {integrity: sha512-v0QtNd81v/xKj4gNKeuAerQ/azeNn/G1B1qMLeXOcV8+4TWlD2j3NV1u8q29SDFBXx/NBq5kyEAO+0mpRgacjA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.16.4
+ '@babel/helper-compilation-targets': 7.16.3
+ '@babel/helper-plugin-utils': 7.14.5
+ '@babel/helper-validator-option': 7.14.5
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.16.2
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.16.0
+ '@babel/plugin-proposal-async-generator-functions': 7.16.4
+ '@babel/plugin-proposal-class-properties': 7.16.0
+ '@babel/plugin-proposal-class-static-block': 7.16.0
+ '@babel/plugin-proposal-dynamic-import': 7.16.0
+ '@babel/plugin-proposal-export-namespace-from': 7.16.0
+ '@babel/plugin-proposal-json-strings': 7.16.0
+ '@babel/plugin-proposal-logical-assignment-operators': 7.16.0
+ '@babel/plugin-proposal-nullish-coalescing-operator': 7.16.0
+ '@babel/plugin-proposal-numeric-separator': 7.16.0
+ '@babel/plugin-proposal-object-rest-spread': 7.16.0
+ '@babel/plugin-proposal-optional-catch-binding': 7.16.0
+ '@babel/plugin-proposal-optional-chaining': 7.16.0
+ '@babel/plugin-proposal-private-methods': 7.16.0
+ '@babel/plugin-proposal-private-property-in-object': 7.16.0
+ '@babel/plugin-proposal-unicode-property-regex': 7.16.0
+ '@babel/plugin-syntax-async-generators': 7.8.4
+ '@babel/plugin-syntax-class-properties': 7.12.13
+ '@babel/plugin-syntax-class-static-block': 7.14.5
+ '@babel/plugin-syntax-dynamic-import': 7.8.3
+ '@babel/plugin-syntax-export-namespace-from': 7.8.3
+ '@babel/plugin-syntax-json-strings': 7.8.3
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3
+ '@babel/plugin-syntax-numeric-separator': 7.10.4
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3
+ '@babel/plugin-syntax-optional-chaining': 7.8.3
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5
+ '@babel/plugin-syntax-top-level-await': 7.14.5
+ '@babel/plugin-transform-arrow-functions': 7.16.0
+ '@babel/plugin-transform-async-to-generator': 7.16.0
+ '@babel/plugin-transform-block-scoped-functions': 7.16.0
+ '@babel/plugin-transform-block-scoping': 7.16.0
+ '@babel/plugin-transform-classes': 7.16.0
+ '@babel/plugin-transform-computed-properties': 7.16.0
+ '@babel/plugin-transform-destructuring': 7.16.0
+ '@babel/plugin-transform-dotall-regex': 7.16.0
+ '@babel/plugin-transform-duplicate-keys': 7.16.0
+ '@babel/plugin-transform-exponentiation-operator': 7.16.0
+ '@babel/plugin-transform-for-of': 7.16.0
+ '@babel/plugin-transform-function-name': 7.16.0
+ '@babel/plugin-transform-literals': 7.16.0
+ '@babel/plugin-transform-member-expression-literals': 7.16.0
+ '@babel/plugin-transform-modules-amd': 7.16.0
+ '@babel/plugin-transform-modules-commonjs': 7.16.0
+ '@babel/plugin-transform-modules-systemjs': 7.16.0
+ '@babel/plugin-transform-modules-umd': 7.16.0
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.16.0
+ '@babel/plugin-transform-new-target': 7.16.0
+ '@babel/plugin-transform-object-super': 7.16.0
+ '@babel/plugin-transform-parameters': 7.16.3
+ '@babel/plugin-transform-property-literals': 7.16.0
+ '@babel/plugin-transform-regenerator': 7.16.0
+ '@babel/plugin-transform-reserved-words': 7.16.0
+ '@babel/plugin-transform-shorthand-properties': 7.16.0
+ '@babel/plugin-transform-spread': 7.16.0
+ '@babel/plugin-transform-sticky-regex': 7.16.0
+ '@babel/plugin-transform-template-literals': 7.16.0
+ '@babel/plugin-transform-typeof-symbol': 7.16.0
+ '@babel/plugin-transform-unicode-escapes': 7.16.0
+ '@babel/plugin-transform-unicode-regex': 7.16.0
+ '@babel/preset-modules': 0.1.5
+ '@babel/types': 7.16.0
+ babel-plugin-polyfill-corejs2: 0.3.0
+ babel-plugin-polyfill-corejs3: 0.4.0
+ babel-plugin-polyfill-regenerator: 0.3.0
+ core-js-compat: 3.19.1
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@babel/preset-modules/0.1.4_@babel+core@7.13.16:
resolution: {integrity: sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==}
peerDependencies:
@@ -4915,6 +5076,13 @@ packages:
regenerator-runtime: 0.13.9
dev: true
+ /@babel/runtime/7.16.3:
+ resolution: {integrity: sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ regenerator-runtime: 0.13.9
+ dev: true
+
/@babel/template/7.14.5:
resolution: {integrity: sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==}
engines: {node: '>=6.9.0'}
@@ -4967,6 +5135,23 @@ packages:
- supports-color
dev: true
+ /@babel/traverse/7.16.3:
+ resolution: {integrity: sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.16.0
+ '@babel/generator': 7.16.0
+ '@babel/helper-function-name': 7.16.0
+ '@babel/helper-hoist-variables': 7.16.0
+ '@babel/helper-split-export-declaration': 7.16.0
+ '@babel/parser': 7.16.4
+ '@babel/types': 7.16.0
+ debug: 4.3.2
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@babel/types/7.15.0:
resolution: {integrity: sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==}
engines: {node: '>=6.9.0'}
@@ -5761,9 +5946,9 @@ packages:
'@babel/core': '>=7'
dependencies:
'@babel/generator': 7.16.0
- '@babel/plugin-transform-runtime': 7.16.0
+ '@babel/plugin-transform-runtime': 7.16.4
'@babel/plugin-transform-template-literals': 7.16.0
- '@babel/preset-env': 7.16.0
+ '@babel/preset-env': 7.16.4
'@linaria/babel-preset': 3.0.0-beta.13
'@linaria/logger': 3.0.0-beta.3
'@linaria/preeval': 3.0.0-beta.13
@@ -5893,7 +6078,7 @@ packages:
resolve: 1.17.0
semver: 7.3.4
source-map: 0.6.1
- typescript: 4.1.3
+ typescript: 4.1.6
dev: true
/@microsoft/tsdoc/0.12.24:
@@ -10190,7 +10375,7 @@ packages:
'@types/yargs-parser': 20.2.1
dev: true
- /@typescript-eslint/eslint-plugin/4.14.0_980e7d90d2d08155204a38366bd3b934:
+ /@typescript-eslint/eslint-plugin/4.14.0_4f40ec8f9ae74407a8c29890901bb23f:
resolution: {integrity: sha512-IJ5e2W7uFNfg4qh9eHkHRUCbgZ8VKtGwD07kannJvM5t/GU8P8+24NX8gi3Hf5jST5oWPY8kyV1s/WtfiZ4+Ww==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -10201,8 +10386,8 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/experimental-utils': 4.14.0_eslint@7.18.0+typescript@4.1.3
- '@typescript-eslint/parser': 4.14.0_eslint@7.18.0+typescript@4.1.3
+ '@typescript-eslint/experimental-utils': 4.14.0_eslint@7.18.0+typescript@4.4.4
+ '@typescript-eslint/parser': 4.14.0_eslint@7.18.0+typescript@4.4.4
'@typescript-eslint/scope-manager': 4.14.0
debug: 4.3.1
eslint: 7.18.0
@@ -10210,8 +10395,8 @@ packages:
lodash: 4.17.20
regexpp: 3.1.0
semver: 7.3.4
- tsutils: 3.19.1_typescript@4.1.3
- typescript: 4.1.3
+ tsutils: 3.19.1_typescript@4.4.4
+ typescript: 4.4.4
transitivePeerDependencies:
- supports-color
dev: true
@@ -10258,7 +10443,7 @@ packages:
- typescript
dev: true
- /@typescript-eslint/experimental-utils/4.14.0_eslint@7.18.0+typescript@4.1.3:
+ /@typescript-eslint/experimental-utils/4.14.0_eslint@7.18.0+typescript@4.4.4:
resolution: {integrity: sha512-6i6eAoiPlXMKRbXzvoQD5Yn9L7k9ezzGRvzC/x1V3650rUk3c3AOjQyGYyF9BDxQQDK2ElmKOZRD0CbtdkMzQQ==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -10267,7 +10452,7 @@ packages:
'@types/json-schema': 7.0.7
'@typescript-eslint/scope-manager': 4.14.0
'@typescript-eslint/types': 4.14.0
- '@typescript-eslint/typescript-estree': 4.14.0_typescript@4.1.3
+ '@typescript-eslint/typescript-estree': 4.14.0_typescript@4.4.4
eslint: 7.18.0
eslint-scope: 5.1.1
eslint-utils: 2.1.0
@@ -10294,7 +10479,7 @@ packages:
- typescript
dev: true
- /@typescript-eslint/parser/4.14.0_eslint@7.18.0+typescript@4.1.3:
+ /@typescript-eslint/parser/4.14.0_eslint@7.18.0+typescript@4.4.4:
resolution: {integrity: sha512-sUDeuCjBU+ZF3Lzw0hphTyScmDDJ5QVkyE21pRoBo8iDl7WBtVFS+WDN3blY1CH3SBt7EmYCw6wfmJjF0l/uYg==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -10306,15 +10491,15 @@ packages:
dependencies:
'@typescript-eslint/scope-manager': 4.14.0
'@typescript-eslint/types': 4.14.0
- '@typescript-eslint/typescript-estree': 4.14.0_typescript@4.1.3
+ '@typescript-eslint/typescript-estree': 4.14.0_typescript@4.4.4
debug: 4.3.1
eslint: 7.18.0
- typescript: 4.1.3
+ typescript: 4.4.4
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/parser/4.4.1_eslint@7.18.0+typescript@4.1.3:
+ /@typescript-eslint/parser/4.4.1_eslint@7.18.0+typescript@4.4.4:
resolution: {integrity: sha512-S0fuX5lDku28Au9REYUsV+hdJpW/rNW0gWlc4SXzF/kdrRaAVX9YCxKpziH7djeWT/HFAjLZcnY7NJD8xTeUEg==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -10326,10 +10511,10 @@ packages:
dependencies:
'@typescript-eslint/scope-manager': 4.4.1
'@typescript-eslint/types': 4.4.1
- '@typescript-eslint/typescript-estree': 4.4.1_typescript@4.1.3
+ '@typescript-eslint/typescript-estree': 4.4.1_typescript@4.4.4
debug: 4.3.1
eslint: 7.18.0
- typescript: 4.1.3
+ typescript: 4.4.4
transitivePeerDependencies:
- supports-color
dev: true
@@ -10414,7 +10599,7 @@ packages:
- supports-color
dev: true
- /@typescript-eslint/typescript-estree/4.14.0_typescript@4.1.3:
+ /@typescript-eslint/typescript-estree/4.14.0_typescript@4.4.4:
resolution: {integrity: sha512-wRjZ5qLao+bvS2F7pX4qi2oLcOONIB+ru8RGBieDptq/SudYwshveORwCVU4/yMAd4GK7Fsf8Uq1tjV838erag==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -10430,13 +10615,13 @@ packages:
is-glob: 4.0.1
lodash: 4.17.20
semver: 7.3.4
- tsutils: 3.19.1_typescript@4.1.3
- typescript: 4.1.3
+ tsutils: 3.19.1_typescript@4.4.4
+ typescript: 4.4.4
transitivePeerDependencies:
- supports-color
dev: true
- /@typescript-eslint/typescript-estree/4.4.1_typescript@4.1.3:
+ /@typescript-eslint/typescript-estree/4.4.1_typescript@4.4.4:
resolution: {integrity: sha512-wP/V7ScKzgSdtcY1a0pZYBoCxrCstLrgRQ2O9MmCUZDtmgxCO/TCqOTGRVwpP4/2hVfqMz/Vw1ZYrG8cVxvN3g==}
engines: {node: ^10.12.0 || >=12.0.0}
peerDependencies:
@@ -10452,8 +10637,8 @@ packages:
is-glob: 4.0.1
lodash: 4.17.20
semver: 7.3.4
- tsutils: 3.19.1_typescript@4.1.3
- typescript: 4.1.3
+ tsutils: 3.19.1_typescript@4.4.4
+ typescript: 4.4.4
transitivePeerDependencies:
- supports-color
dev: true
@@ -10800,7 +10985,6 @@ packages:
ajv: ^6.9.1
dependencies:
ajv: 6.12.6
- dev: true
/ajv/6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -10809,7 +10993,6 @@ packages:
fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1
uri-js: 4.4.1
- dev: true
/ajv/7.0.3:
resolution: {integrity: sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==}
@@ -11642,6 +11825,18 @@ packages:
- supports-color
dev: true
+ /babel-plugin-polyfill-corejs2/0.3.0:
+ resolution: {integrity: sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/compat-data': 7.16.4
+ '@babel/helper-define-polyfill-provider': 0.3.0
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/babel-plugin-polyfill-corejs3/0.1.7_@babel+core@7.15.0:
resolution: {integrity: sha512-u+gbS9bbPhZWEeyy1oR/YaaSpod/KDT07arZHb80aTpl8H5ZBq+uN1nN9/xtX7jQyfLdPfoqI4Rue/MQSWJquw==}
peerDependencies:
@@ -11713,6 +11908,17 @@ packages:
- supports-color
dev: true
+ /babel-plugin-polyfill-corejs3/0.4.0:
+ resolution: {integrity: sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/helper-define-polyfill-provider': 0.3.0
+ core-js-compat: 3.19.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/babel-plugin-polyfill-regenerator/0.2.2_@babel+core@7.13.16:
resolution: {integrity: sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg==}
peerDependencies:
@@ -11756,6 +11962,16 @@ packages:
- supports-color
dev: true
+ /babel-plugin-polyfill-regenerator/0.3.0:
+ resolution: {integrity: sha512-dhAPTDLGoMW5/84wkgwiLRwMnio2i1fUe53EuvtKMv0pn2p3S8OCoV1xAzfJPl0KOX7IB89s2ib85vbYiea3jg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/helper-define-polyfill-provider': 0.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/babel-plugin-syntax-jsx/6.18.0:
resolution: {integrity: sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=}
dev: true
@@ -11871,6 +12087,17 @@ packages:
pascalcase: 0.1.1
dev: true
+ /base64-inline-loader/1.1.1:
+ resolution: {integrity: sha512-v/bHvXQ8sW28t9ZwBsFGgyqZw2bpT49/dtPTtlmixoSM/s9wnOngOKFVQLRH8BfMTy6jTl5m5CdvqpZt8y5d6g==}
+ engines: {node: '>=6.2', npm: '>=3.8'}
+ peerDependencies:
+ webpack: ^4.x
+ dependencies:
+ file-loader: 1.1.11
+ loader-utils: 1.4.0
+ mime-types: 2.1.33
+ dev: false
+
/base64-js/1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true
@@ -11907,7 +12134,6 @@ packages:
/big.js/5.2.2:
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
- dev: true
/binary-extensions/1.13.1:
resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==}
@@ -12144,6 +12370,18 @@ packages:
picocolors: 1.0.0
dev: true
+ /browserslist/4.18.1:
+ resolution: {integrity: sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+ dependencies:
+ caniuse-lite: 1.0.30001280
+ electron-to-chromium: 1.3.899
+ escalade: 3.1.1
+ node-releases: 2.0.1
+ picocolors: 1.0.0
+ dev: true
+
/bser/2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
dependencies:
@@ -12382,6 +12620,10 @@ packages:
resolution: {integrity: sha512-psUNoaG1ilknZPxi8HuhQWobuhLqtYSRUxplfVkEJdgZNB9TETVYGSBtv4YyfAdGvE6gn2eb0ztiXqHoWJcGnw==}
dev: true
+ /caniuse-lite/1.0.30001280:
+ resolution: {integrity: sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA==}
+ dev: true
+
/capture-exit/2.0.0:
resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==}
engines: {node: 6.* || 8.* || >= 10.*}
@@ -13040,7 +13282,7 @@ packages:
/core-js-compat/3.19.1:
resolution: {integrity: sha512-Q/VJ7jAF/y68+aUsQJ/afPOewdsGkDtcMb40J8MbuWKlK3Y+wtHq8bTHKPj2WKWLIqmS5JhHs4CzHtz6pT2W6g==}
dependencies:
- browserslist: 4.17.6
+ browserslist: 4.18.1
semver: 7.0.0
dev: true
@@ -13794,7 +14036,7 @@ packages:
dependencies:
globby: 11.0.4
graceful-fs: 4.2.8
- is-glob: 4.0.1
+ is-glob: 4.0.3
is-path-cwd: 2.2.0
is-path-inside: 3.0.3
p-map: 4.0.0
@@ -14129,6 +14371,10 @@ packages:
resolution: {integrity: sha512-5iD1zgyPpFER4kJ716VsA4MxQ6x405dxdFNCEK2mITL075VHO5ResjY0xzQUZguCww/KlBxCA6JmBA9sDt1PRw==}
dev: true
+ /electron-to-chromium/1.3.899:
+ resolution: {integrity: sha512-w16Dtd2zl7VZ4N4Db+FIa7n36sgPGCKjrKvUUmp5ialsikvcQLjcJR9RWnlYNxIyEHLdHaoIZEqKsPxU9MdyBg==}
+ dev: true
+
/element-resize-detector/1.2.3:
resolution: {integrity: sha512-+dhNzUgLpq9ol5tyhoG7YLoXL3ssjfFW+0gpszXPwRU6NjGr1fVHMEAF8fVzIiRJq57Nre0RFeIjJwI8Nh2NmQ==}
dependencies:
@@ -14177,7 +14423,6 @@ packages:
/emojis-list/3.0.0:
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
engines: {node: '>= 4'}
- dev: true
/emotion-theming/10.0.27_5f216699bc8c1f24088b3bf77b7cbbdf:
resolution: {integrity: sha512-MlF1yu/gYh8u+sLUqA0YuA9JX0P4Hb69WlKc/9OLo+WCXuX6sy/KoIa+qJimgmr2dWqnypYKYPX37esjDBbhdw==}
@@ -14535,13 +14780,13 @@ packages:
object.entries: 1.1.3
dev: true
- /eslint-config-airbnb-typescript/12.0.0_aa91c0ea1e61103ae60b9cd49dfd9775:
+ /eslint-config-airbnb-typescript/12.0.0_b55a7168bd2ecdf8767ddb224d20fd7e:
resolution: {integrity: sha512-TUCVru1Z09eKnVAX5i3XoNzjcCOU3nDQz2/jQGkg1jVYm+25fKClveziSl16celfCq+npU0MBPW/ZnXdGFZ9lw==}
peerDependencies:
'@typescript-eslint/eslint-plugin': ^4.4.1
dependencies:
- '@typescript-eslint/eslint-plugin': 4.14.0_980e7d90d2d08155204a38366bd3b934
- '@typescript-eslint/parser': 4.4.1_eslint@7.18.0+typescript@4.1.3
+ '@typescript-eslint/eslint-plugin': 4.14.0_4f40ec8f9ae74407a8c29890901bb23f
+ '@typescript-eslint/parser': 4.4.1_eslint@7.18.0+typescript@4.4.4
eslint-config-airbnb: 18.2.0_8b932c4aedefa0fbb298d8c6e2d8003e
eslint-config-airbnb-base: 14.2.0_d4477e7d44043beb7952cd76bd313965
transitivePeerDependencies:
@@ -15174,7 +15419,6 @@ packages:
/fast-deep-equal/3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
- dev: true
/fast-diff/1.2.0:
resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
@@ -15217,7 +15461,6 @@ packages:
/fast-json-stable-stringify/2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
- dev: true
/fast-levenshtein/2.0.6:
resolution: {integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=}
@@ -15296,6 +15539,16 @@ packages:
flat-cache: 3.0.4
dev: true
+ /file-loader/1.1.11:
+ resolution: {integrity: sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==}
+ engines: {node: '>= 4.3 < 5.0.0 || >= 5.10'}
+ peerDependencies:
+ webpack: ^2.0.0 || ^3.0.0 || ^4.0.0
+ dependencies:
+ loader-utils: 1.4.0
+ schema-utils: 0.4.7
+ dev: false
+
/file-loader/6.2.0_webpack@4.46.0:
resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==}
engines: {node: '>= 10.13.0'}
@@ -16121,7 +16374,7 @@ packages:
source-map: 0.6.1
wordwrap: 1.0.0
optionalDependencies:
- uglify-js: 3.12.5
+ uglify-js: 3.14.3
dev: true
/har-schema/2.0.0:
@@ -18694,7 +18947,6 @@ packages:
/json-schema-traverse/0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
- dev: true
/json-schema-traverse/1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
@@ -18730,7 +18982,6 @@ packages:
hasBin: true
dependencies:
minimist: 1.2.5
- dev: true
/json5/2.1.3:
resolution: {integrity: sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==}
@@ -18757,7 +19008,7 @@ packages:
/jsonfile/4.0.0:
resolution: {integrity: sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=}
optionalDependencies:
- graceful-fs: 4.2.8
+ graceful-fs: 4.2.4
dev: true
/jsonfile/6.1.0:
@@ -18932,7 +19183,7 @@ packages:
resolution: {integrity: sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=}
engines: {node: '>=4'}
dependencies:
- graceful-fs: 4.2.4
+ graceful-fs: 4.2.8
parse-json: 2.2.0
pify: 2.3.0
strip-bom: 3.0.0
@@ -18970,7 +19221,6 @@ packages:
big.js: 5.2.2
emojis-list: 3.0.0
json5: 1.0.1
- dev: true
/loader-utils/2.0.0:
resolution: {integrity: sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==}
@@ -19420,14 +19670,12 @@ packages:
/mime-db/1.50.0:
resolution: {integrity: sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==}
engines: {node: '>= 0.6'}
- dev: true
/mime-types/2.1.33:
resolution: {integrity: sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.50.0
- dev: true
/mime/1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
@@ -19501,7 +19749,6 @@ packages:
/minimist/1.2.5:
resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==}
- dev: true
/minipass-collect/1.0.2:
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
@@ -21781,6 +22028,12 @@ packages:
hasBin: true
dev: true
+ /prettier/2.4.1:
+ resolution: {integrity: sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+ dev: true
+
/pretty-bytes/4.0.2:
resolution: {integrity: sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=}
engines: {node: '>=4'}
@@ -22013,7 +22266,6 @@ packages:
/punycode/2.1.1:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
engines: {node: '>=6'}
- dev: true
/pupa/2.1.1:
resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==}
@@ -22631,7 +22883,7 @@ packages:
/regenerator-transform/0.14.5:
resolution: {integrity: sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==}
dependencies:
- '@babel/runtime': 7.16.0
+ '@babel/runtime': 7.16.3
dev: true
/regex-not/1.0.2:
@@ -23312,6 +23564,14 @@ packages:
object-assign: 4.1.1
dev: true
+ /schema-utils/0.4.7:
+ resolution: {integrity: sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==}
+ engines: {node: '>= 4'}
+ dependencies:
+ ajv: 6.12.6
+ ajv-keywords: 3.5.2_ajv@6.12.6
+ dev: false
+
/schema-utils/1.0.0:
resolution: {integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==}
engines: {node: '>= 4'}
@@ -24807,14 +25067,14 @@ packages:
/tslib/2.3.1:
resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==}
- /tsutils/3.19.1_typescript@4.1.3:
+ /tsutils/3.19.1_typescript@4.4.4:
resolution: {integrity: sha512-GEdoBf5XI324lu7ycad7s6laADfnAqCw6wLGI+knxvw9vsIYBaJfYdmeCEG3FMMUiSm3OGgNb+m6utsWf5h9Vw==}
engines: {node: '>= 6'}
peerDependencies:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
dependencies:
tslib: 1.14.1
- typescript: 4.1.3
+ typescript: 4.4.4
dev: true
/tsutils/3.21.0_typescript@4.4.4:
@@ -24939,12 +25199,39 @@ packages:
typescript: 4.1.3
dev: true
+ /typedoc/0.20.16_typescript@4.4.4:
+ resolution: {integrity: sha512-xqIL8lT6ZE3QpP0GN30ckeTR05NSEkrP2pXQlNhC0OFkbvnjqJtDUcWSmCO15BuYyu4qsEbZT+tKYFEAt9Jxew==}
+ engines: {node: '>= 10.8.0'}
+ hasBin: true
+ peerDependencies:
+ typescript: 3.9.x || 4.0.x || 4.1.x
+ dependencies:
+ colors: 1.4.0
+ fs-extra: 9.1.0
+ handlebars: 4.7.6
+ lodash: 4.17.20
+ lunr: 2.3.9
+ marked: 1.2.7
+ minimatch: 3.0.4
+ progress: 2.0.3
+ shelljs: 0.8.4
+ shiki: 0.2.7
+ typedoc-default-themes: 0.12.4
+ typescript: 4.4.4
+ dev: true
+
/typescript/4.1.3:
resolution: {integrity: sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: true
+ /typescript/4.1.6:
+ resolution: {integrity: sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==}
+ engines: {node: '>=4.2.0'}
+ hasBin: true
+ dev: true
+
/typescript/4.2.3:
resolution: {integrity: sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==}
engines: {node: '>=4.2.0'}
@@ -24969,8 +25256,8 @@ packages:
hasBin: true
dev: true
- /uglify-js/3.12.5:
- resolution: {integrity: sha512-SgpgScL4T7Hj/w/GexjnBHi3Ien9WS1Rpfg5y91WXMj9SY997ZCQU76mH4TpLwwfmMvoOU8wiaRkIf6NaH3mtg==}
+ /uglify-js/3.14.3:
+ resolution: {integrity: sha512-mic3aOdiq01DuSVx0TseaEzMIVqebMZ0Z3vaeDhFEh9bsc24hV1TFvN74reA2vs08D0ZWfNjAcJ3UbVLaBss+g==}
engines: {node: '>=0.8.0'}
hasBin: true
dev: true
@@ -25212,7 +25499,6 @@ packages:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies:
punycode: 2.1.1
- dev: true
/urix/0.1.0:
resolution: {integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=}