From 8e716879e2774cebf81e4fcb3164f0e23e115799 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Fri, 6 Oct 2023 14:02:55 +0200 Subject: [PATCH] frontend: add prompt to connect keystore --- backend/accounts.go | 34 ++++++++++++++- backend/backend.go | 5 +++ backend/handlers/handlers.go | 7 ++++ frontends/web/src/api/backend.ts | 26 ++++++++++++ frontends/web/src/app.tsx | 2 + .../src/components/keystoreconnectprompt.tsx | 41 +++++++++++++++++++ frontends/web/src/locales/en/app.json | 4 ++ 7 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 frontends/web/src/components/keystoreconnectprompt.tsx diff --git a/backend/accounts.go b/backend/accounts.go index cf2e58124d..d378b6f20e 100644 --- a/backend/accounts.go +++ b/backend/accounts.go @@ -536,11 +536,43 @@ func (backend *Backend) createAndAddAccount(coin coinpkg.Coin, persistedConfig * DBFolder: backend.arguments.CacheDirectoryPath(), NotesFolder: backend.arguments.NotesDirectoryPath(), ConnectKeystore: func() (keystore.Keystore, error) { + type data struct { + Type string `json:"typ"` + KeystoreName string `json:"keystoreName"` + } accountRootFingerprint, err := persistedConfig.SigningConfigurations.RootFingerprint() if err != nil { return nil, err } - return backend.connectKeystore.connect(backend.Keystore(), accountRootFingerprint, 20*time.Minute) + keystoreName := "" + persistedKeystore, err := backend.config.AccountsConfig().LookupKeystore(accountRootFingerprint) + if err == nil { + keystoreName = persistedKeystore.Name + } + backend.Notify(observable.Event{ + Subject: "connect-keystore", + Action: action.Replace, + Object: data{ + Type: "connect", + KeystoreName: keystoreName, + }, + }) + ks, err := backend.connectKeystore.connect( + backend.Keystore(), + accountRootFingerprint, + 20*time.Minute, + ) + // If a previous connect-keystore request is in progress, this one failed, but we don't + // dismiss the previous prompt. We dismiss it only if it is canceled, it timed out, or + // there is some other problem. + if errp.Cause(err) != errInProgress { + backend.Notify(observable.Event{ + Subject: "connect-keystore", + Action: action.Replace, + Object: nil, + }) + } + return ks, err }, OnEvent: func(event accountsTypes.Event) { backend.events <- AccountEvent{ diff --git a/backend/backend.go b/backend/backend.go index 7dec5b4148..142932fcca 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -784,3 +784,8 @@ func (backend *Backend) GetAccountFromCode(code string) (accounts.Interface, err return acct, nil } + +// CancelConnectKeystore cancels a pending keystore connection request if one exists. +func (backend *Backend) CancelConnectKeystore() { + backend.connectKeystore.cancel() +} diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 1777676a39..968c69f00d 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -103,6 +103,7 @@ type Backend interface { AOPPChooseAccount(code accountsTypes.Code) GetAccountFromCode(code string) (accounts.Interface, error) HTTPClient() *http.Client + CancelConnectKeystore() } // Handlers provides a web api to the backend. @@ -232,6 +233,7 @@ func NewHandlers( getAPIRouterNoError(apiRouter)("/aopp/cancel", handlers.postAOPPCancelHandler).Methods("POST") getAPIRouterNoError(apiRouter)("/aopp/approve", handlers.postAOPPApproveHandler).Methods("POST") getAPIRouter(apiRouter)("/aopp/choose-account", handlers.postAOPPChooseAccountHandler).Methods("POST") + getAPIRouter(apiRouter)("/cancel-connect-keystore", handlers.postCancelConnectKeystoreHandler).Methods("POST") devicesRouter := getAPIRouterNoError(apiRouter.PathPrefix("/devices").Subrouter()) devicesRouter("/registered", handlers.getDevicesRegisteredHandler).Methods("GET") @@ -1281,3 +1283,8 @@ func (handlers *Handlers) postAOPPApproveHandler(r *http.Request) interface{} { handlers.backend.AOPPApprove() return nil } + +func (handlers *Handlers) postCancelConnectKeystoreHandler(r *http.Request) (interface{}, error) { + handlers.backend.CancelConnectKeystore() + return nil, nil +} diff --git a/frontends/web/src/api/backend.ts b/frontends/web/src/api/backend.ts index 7eacb7df5a..6b3fa7fc58 100644 --- a/frontends/web/src/api/backend.ts +++ b/frontends/web/src/api/backend.ts @@ -17,6 +17,7 @@ import { AccountCode, CoinCode } from './account'; import { apiGet, apiPost } from '../utils/request'; import { FailResponse, SuccessResponse } from './response'; +import { TSubscriptionCallback, subscribeEndpoint } from './subscribe'; export interface ICoin { coinCode: CoinCode; @@ -74,3 +75,28 @@ export const getDefaultConfig = (): Promise => { export const socksProxyCheck = (proxyAddress: string): Promise => { return apiPost('socksproxy/check', proxyAddress); }; + +export type TSyncConnectKeystore = null | { + typ: 'connect'; + keystoreName: string; +}; + +/** + * Returns a function that subscribes a callback on a "connect-keystore". + * Meant to be used with `useSubscribe`. + */ +export const syncConnectKeystore = () => { + return ( + cb: TSubscriptionCallback + ) => { + return subscribeEndpoint('connect-keystore', ( + obj: TSyncConnectKeystore, + ) => { + cb(obj); + }); + }; +}; + +export const cancelConnectKeystore = (): Promise => { + return apiPost('cancel-connect-keystore'); +}; diff --git a/frontends/web/src/app.tsx b/frontends/web/src/app.tsx index d620f78708..9052f14d12 100644 --- a/frontends/web/src/app.tsx +++ b/frontends/web/src/app.tsx @@ -29,6 +29,7 @@ import { Alert } from './components/alert/Alert'; import { Aopp } from './components/aopp/aopp'; import { Banner } from './components/banner/banner'; import { Confirm } from './components/confirm/Confirm'; +import { KeystoreConnectPrompt } from './components/keystoreconnectprompt'; import { panelStore } from './components/sidebar/sidebar'; import { MobileDataWarning } from './components/mobiledatawarning'; import { Sidebar, toggleSidebar } from './components/sidebar/sidebar'; @@ -208,6 +209,7 @@ class App extends Component { + { Object.entries(devices).map(([deviceID, productName]) => { if (productName === 'bitbox02') { diff --git a/frontends/web/src/components/keystoreconnectprompt.tsx b/frontends/web/src/components/keystoreconnectprompt.tsx new file mode 100644 index 0000000000..badb88669f --- /dev/null +++ b/frontends/web/src/components/keystoreconnectprompt.tsx @@ -0,0 +1,41 @@ +/** + * Copyright 2023 Shift Crypto AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useTranslation } from 'react-i18next'; +import { cancelConnectKeystore, syncConnectKeystore, TSyncConnectKeystore } from '../api/backend'; +import { useSubscribe } from '../hooks/api'; + +export function KeystoreConnectPrompt() { + const { t } = useTranslation(); + const data: undefined | TSyncConnectKeystore = useSubscribe(syncConnectKeystore()); + if (!data) { + return null; + } + switch (data.typ) { + case 'connect': + return ( + <> + { data.keystoreName === '' ? + t('connectKeystore.promptNoName') : + t('connectKeystore.promptWithName', { name: data.keystoreName }) + } + + + ); + default: + return null; + } +} diff --git a/frontends/web/src/locales/en/app.json b/frontends/web/src/locales/en/app.json index 3409146ce6..12103da73b 100644 --- a/frontends/web/src/locales/en/app.json +++ b/frontends/web/src/locales/en/app.json @@ -486,6 +486,10 @@ "infoWhenPaired": "First on the paired mobile and then your BitBox" }, "confirmOnDevice": "Please confirm on your device.", + "connectKeystore": { + "promptNoName": "Please connect your BitBox02", + "promptWithName": "Please connect your BitBox02 named {{name}}" + }, "darkmode": { "toggle": "Dark mode" },