From 3a3877c345e8e2645bc5e692110c0afa8ed7d35f Mon Sep 17 00:00:00 2001 From: Bao Hoang <142210850+bao-sotatek@users.noreply.github.com> Date: Fri, 31 May 2024 15:44:56 +0700 Subject: [PATCH 01/28] refactor(core): remove the core/cardano/addresses dependency (#499) * feat: remove the core/cardano/addresses file and any related dependencies * feat: add unittest and remove unused dependencies * chore: remove unused de pendency --- jest.config.ts | 4 +- package-lock.json | 13 ----- package.json | 2 - src/core/cardano/addresses.test.ts | 47 ------------------- src/core/cardano/addresses.ts | 27 ----------- src/core/cardano/addresses.types.ts | 6 --- src/core/cardano/index.ts | 1 - .../storage/secureStorage/secureStorage.ts | 2 - src/ui/components/AppWrapper/AppWrapper.tsx | 7 +-- .../VerifySeedPhrase.test.tsx | 46 +++--------------- .../VerifySeedPhrase/VerifySeedPhrase.tsx | 21 +-------- 11 files changed, 12 insertions(+), 164 deletions(-) delete mode 100644 src/core/cardano/addresses.test.ts delete mode 100644 src/core/cardano/addresses.ts delete mode 100644 src/core/cardano/addresses.types.ts delete mode 100644 src/core/cardano/index.ts diff --git a/jest.config.ts b/jest.config.ts index 1f7a84c88..5c1dccff3 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -27,15 +27,13 @@ export default { "tsx", "json", "node", - "yaml" + "yaml", ], moduleNameMapper: { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/ui/__mocks__/fileMock.ts", "\\.(css|scss)$": "/src/ui/__mocks__/styleMock.ts", // Jest cannot import the browser version so we can map in the NodeJS version instead. - "@dcspark/cardano-multiplatform-lib-browser": - "@dcspark/cardano-multiplatform-lib-nodejs", }, testEnvironment: "jsdom", testMatch: ["**/src/**/?(*.)+(test).[tj]s?(x)"], diff --git a/package-lock.json b/package-lock.json index 2c1f27057..16002d7e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "@capacitor/share": "^5.0.0", "@capacitor/splash-screen": "^5.0.0", "@capacitor/status-bar": "^5.0.7", - "@dcspark/cardano-multiplatform-lib-browser": "^3.1.2", "@fabianbormann/cardano-peer-connect": "^1.2.16", "@ionic/react": "^7.5.4", "@ionic/react-router": "^7.5.4", @@ -49,7 +48,6 @@ "devDependencies": { "@capacitor/assets": "^3.0.1", "@capacitor/cli": "^5.0.0", - "@dcspark/cardano-multiplatform-lib-nodejs": "^3.1.2", "@faker-js/faker": "^8.4.1", "@ionic/react-test-utils": "^0.4.0", "@testing-library/dom": "^9.3.1", @@ -2988,17 +2986,6 @@ "kuler": "^2.0.0" } }, - "node_modules/@dcspark/cardano-multiplatform-lib-browser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@dcspark/cardano-multiplatform-lib-browser/-/cardano-multiplatform-lib-browser-3.1.2.tgz", - "integrity": "sha512-BDIF33nqEKAGn72+ESD1fLgnWCZJbPWnVKHr0cy7Al2zhvM30rGsajcJNHLPh2XXZQh1H35fBiHHR2uUyZRRpw==" - }, - "node_modules/@dcspark/cardano-multiplatform-lib-nodejs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@dcspark/cardano-multiplatform-lib-nodejs/-/cardano-multiplatform-lib-nodejs-3.1.2.tgz", - "integrity": "sha512-bfFI7ljC8El+yj4kG2G7GXcoHHhWJyJKonJ64H3Kcbns4+tHOTy525d1q/u4+hG+G42JPw4Zj5CFIBLv5GLMxg==", - "dev": true - }, "node_modules/@devicefarmer/adbkit-apkreader": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-apkreader/-/adbkit-apkreader-3.2.4.tgz", diff --git a/package.json b/package.json index 657e88789..7a76ad48f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@capacitor/share": "^5.0.0", "@capacitor/splash-screen": "^5.0.0", "@capacitor/status-bar": "^5.0.7", - "@dcspark/cardano-multiplatform-lib-browser": "^3.1.2", "@fabianbormann/cardano-peer-connect": "^1.2.16", "@ionic/react": "^7.5.4", "@ionic/react-router": "^7.5.4", @@ -56,7 +55,6 @@ "devDependencies": { "@capacitor/assets": "^3.0.1", "@capacitor/cli": "^5.0.0", - "@dcspark/cardano-multiplatform-lib-nodejs": "^3.1.2", "@faker-js/faker": "^8.4.1", "@ionic/react-test-utils": "^0.4.0", "@testing-library/dom": "^9.3.1", diff --git a/src/core/cardano/addresses.test.ts b/src/core/cardano/addresses.test.ts deleted file mode 100644 index f339ce488..000000000 --- a/src/core/cardano/addresses.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Addresses } from "./addresses"; - -const validSeedPhrase15Words = - "test walk nut penalty hip pave soap entry language right filter choice"; -const entropy15Words = "df9ed25ed146bf43336a5d7cf7395994"; -const rootXprvKey15Words = - "xprv1vzrzr76vqyqlavclduhawqvtae2pq8lk0424q7t8rzfjyhhp530zxv2fwq5a3pd4vdzqtu6s2zxdjhww8xg4qwcs7y5dqne5k7mz27p6rcaath83rl20nz0v9nwdaga9fkufjuucza8vmny8qpkzwstk5quneyk9"; -const validSeedPhrase24Words = - "find bag dilemma sing symptom page sand exotic celery tape cat typical sea portion jar return trophy warfare tribe soap protect tuna goddess shine"; -const entropy24Words = - "56a230f8e4adc93defca80251bb88f75fc1f509dd5c1e8fee3a166dacbd4d90e"; -const rootXprvKey24Words = - "xprv18pntsvwvvxj796fulp593yfepesetg3ahfugd404dxy5r6m9r4x39xsswdjqjm9wzgp5n0mtr7twp9949473a8nywrkv2l6fxc9cvvncvukmjvf7e467a9um26pa5su2tuh9pva770g5qs4jd95zvw3upuhhyvxv"; -const invalidSeedPhrase = "INVALID_SEEDPHRASE"; - -describe("Cardano seed phrase and address derivation", () => { - test("should return the entropy from a seedphrase", async () => { - expect(Addresses.convertToEntropy(validSeedPhrase15Words)).toEqual( - entropy15Words - ); - expect(Addresses.convertToEntropy(validSeedPhrase24Words)).toEqual( - entropy24Words - ); - }); - - test("should throw if an invalid mnemonic is provided", () => { - expect(() => Addresses.convertToEntropy(invalidSeedPhrase)).toThrowError(); - }); - - test("can return a root extended private key hex from an entropy", async () => { - expect(Addresses.entropyToBip32NoPasscode(entropy15Words)).toEqual( - rootXprvKey15Words - ); - expect(Addresses.entropyToBip32NoPasscode(entropy24Words)).toEqual( - rootXprvKey24Words - ); - }); - - test("should return a seedphrase from an entropy", () => { - expect(Addresses.convertToMnemonic(entropy15Words)).toEqual( - validSeedPhrase15Words - ); - expect(Addresses.convertToMnemonic(entropy24Words)).toEqual( - validSeedPhrase24Words - ); - }); -}); diff --git a/src/core/cardano/addresses.ts b/src/core/cardano/addresses.ts deleted file mode 100644 index ce3434fa8..000000000 --- a/src/core/cardano/addresses.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Bip32PrivateKey } from "@dcspark/cardano-multiplatform-lib-browser"; -import { entropyToMnemonic, mnemonicToEntropy } from "bip39"; -import { Buffer } from "buffer"; - -class Addresses { - static convertToEntropy(seedPhrase: string): string { - return mnemonicToEntropy(seedPhrase); - } - - static convertToMnemonic(entropy: string): string { - return entropyToMnemonic(entropy); - } - - static entropyToBip32NoPasscode(entropy: string): string { - return Bip32PrivateKey.from_bip39_entropy( - Buffer.from(entropy, "hex"), - Buffer.from("") - ).to_bech32(); - } - - static bech32ToHexBip32Private(Bech32XPrv: string): string { - const privateKeyBytes = Bip32PrivateKey.from_bech32(Bech32XPrv).as_bytes(); - return Buffer.from(privateKeyBytes).toString("hex"); - } -} - -export { Addresses }; diff --git a/src/core/cardano/addresses.types.ts b/src/core/cardano/addresses.types.ts deleted file mode 100644 index e53fd712b..000000000 --- a/src/core/cardano/addresses.types.ts +++ /dev/null @@ -1,6 +0,0 @@ -enum NetworkType { - MAINNET = "mainnet", - TESTNET = "testnet", -} - -export { NetworkType }; diff --git a/src/core/cardano/index.ts b/src/core/cardano/index.ts deleted file mode 100644 index 1a1cee6c0..000000000 --- a/src/core/cardano/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./addresses"; diff --git a/src/core/storage/secureStorage/secureStorage.ts b/src/core/storage/secureStorage/secureStorage.ts index afa183edf..f18d90c89 100644 --- a/src/core/storage/secureStorage/secureStorage.ts +++ b/src/core/storage/secureStorage/secureStorage.ts @@ -5,8 +5,6 @@ import { enum KeyStoreKeys { APP_PASSCODE = "app-login-passcode", - IDENTITY_ENTROPY = "identity-entropy", - IDENTITY_ROOT_XPRV_KEY = "identity-root-xprv-key", APP_OP_PASSWORD = "app-operations-password", SIGNIFY_BRAN = "signify-bran", MEERKAT_SEED = "app-meerkat-seed", diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index 784fcb6de..64e762f0f 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -214,9 +214,8 @@ const AppWrapper = (props: { children: ReactNode }) => { const loadCacheBasicStorage = async () => { let userName: { userName: string } = { userName: "" }; const passcodeIsSet = await checkKeyStore(KeyStoreKeys.APP_PASSCODE); - const seedPhraseIsSet = await checkKeyStore( - KeyStoreKeys.IDENTITY_ROOT_XPRV_KEY - ); + const seedPhraseIsSet = await checkKeyStore(KeyStoreKeys.SIGNIFY_BRAN); + const passwordIsSet = await checkKeyStore(KeyStoreKeys.APP_OP_PASSWORD); const identifiersFavourites = await Agent.agent.basicStorage.findById( @@ -300,8 +299,6 @@ const AppWrapper = (props: { children: ReactNode }) => { ); if (!appAlreadyInit) { await SecureStorage.delete(KeyStoreKeys.APP_PASSCODE); - await SecureStorage.delete(KeyStoreKeys.IDENTITY_ENTROPY); - await SecureStorage.delete(KeyStoreKeys.IDENTITY_ROOT_XPRV_KEY); await SecureStorage.delete(KeyStoreKeys.APP_OP_PASSWORD); await SecureStorage.delete(KeyStoreKeys.SIGNIFY_BRAN); } diff --git a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx index 693be85ad..5d9a293f9 100644 --- a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx +++ b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx @@ -9,24 +9,15 @@ import { act } from "react-dom/test-utils"; import { Provider } from "react-redux"; import { Route } from "react-router-dom"; import configureStore from "redux-mock-store"; -import { Addresses } from "../../../core/cardano"; -import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; import EN_TRANSLATIONS from "../../../locales/en/en.json"; import { RoutePath } from "../../../routes"; import { GenerateSeedPhrase } from "../GenerateSeedPhrase"; import { VerifySeedPhrase } from "../VerifySeedPhrase"; +import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; const MNEMONIC_WORDS = 18; -const entropy = "entropy"; -const rootKeyBech32 = "rootKeyBech32"; -const rootKeyHex = "rootKeyHex"; - jest.mock("../../../core/storage"); -jest.mock("../../../core/cardano/addresses"); -Addresses.convertToEntropy = jest.fn().mockReturnValue(entropy); -Addresses.entropyToBip32NoPasscode = jest.fn().mockReturnValue(rootKeyBech32); -Addresses.bech32ToHexBip32Private = jest.fn().mockReturnValue(rootKeyHex); describe("Verify Seed Phrase Page", () => { const mockStore = configureStore(); @@ -164,10 +155,6 @@ describe("Verify Seed Phrase Page", () => { queryByText(EN_TRANSLATIONS.verifyseedphrase.alert.fail.text) ).toBeVisible() ); - - expect(Addresses.convertToEntropy).not.toBeCalled(); - expect(Addresses.entropyToBip32NoPasscode).not.toBeCalled(); - expect(SecureStorage.set).not.toBeCalled(); }); test("The user can Verify the Seed Phrase when Onboarding", async () => { @@ -207,25 +194,6 @@ describe("Verify Seed Phrase Page", () => { ); fireEvent.click(continueButton); - - const seedPhraseString = initialState.seedPhraseCache.seedPhrase; - const entropy = Addresses.convertToEntropy(seedPhraseString); - const Bech32XPrv = Addresses.entropyToBip32NoPasscode(seedPhraseString); - expect(Addresses.convertToEntropy).toBeCalledWith(seedPhraseString); - expect(Addresses.entropyToBip32NoPasscode).toBeCalledWith(entropy); - expect(Addresses.bech32ToHexBip32Private).toBeCalledWith(Bech32XPrv); - - expect(SecureStorage.set).toBeCalledWith( - KeyStoreKeys.IDENTITY_ROOT_XPRV_KEY, - rootKeyHex - ); - - await waitFor(() => - expect(SecureStorage.set).toBeCalledWith( - KeyStoreKeys.IDENTITY_ENTROPY, - entropy - ) - ); }); test("The user can Verify the Seed Phrase when generating a new seed phrase", async () => { @@ -266,12 +234,12 @@ describe("Verify Seed Phrase Page", () => { fireEvent.click(continueButton); - const seedPhraseString = initialState.seedPhraseCache.seedPhrase; - const entropy = Addresses.convertToEntropy(seedPhraseString); - const Bech32XPrv = Addresses.entropyToBip32NoPasscode(seedPhraseString); - expect(Addresses.convertToEntropy).toBeCalledWith(seedPhraseString); - expect(Addresses.entropyToBip32NoPasscode).toBeCalledWith(entropy); - expect(Addresses.bech32ToHexBip32Private).toBeCalledWith(Bech32XPrv); + await waitFor(() => + expect(SecureStorage.set).toBeCalledWith( + KeyStoreKeys.SIGNIFY_BRAN, + initialState.seedPhraseCache.bran + ) + ); }); test("calls handleOnBack when back button is clicked", async () => { diff --git a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.tsx b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.tsx index 736dc7516..0e3cf9b83 100644 --- a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.tsx +++ b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.tsx @@ -6,22 +6,17 @@ import { useAppDispatch, useAppSelector } from "../../../store/hooks"; import { Alert as AlertFail } from "../../components/Alert"; import { getSeedPhraseCache } from "../../../store/reducers/seedPhraseCache"; import "./VerifySeedPhrase.scss"; -import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; import { getNextRoute } from "../../../routes/nextRoute"; import { updateReduxState } from "../../../store/utils"; -import { - getStateCache, - setCurrentOperation, -} from "../../../store/reducers/stateCache"; +import { getStateCache } from "../../../store/reducers/stateCache"; import { getBackRoute } from "../../../routes/backRoute"; import { DataProps } from "../../../routes/nextRoute/nextRoute.types"; -import { Addresses } from "../../../core/cardano"; import { PageHeader } from "../../components/PageHeader"; import { PageFooter } from "../../components/PageFooter"; import { SeedPhraseModule } from "../../components/SeedPhraseModule"; import { ResponsivePageLayout } from "../../components/layout/ResponsivePageLayout"; import { useAppIonRouter } from "../../hooks"; -import { OperationType } from "../../globals/types"; +import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; const VerifySeedPhrase = () => { const pageId = "verify-seed-phrase"; @@ -75,19 +70,7 @@ const VerifySeedPhrase = () => { }; const storeIdentitySeedPhrase = async () => { - // @TODO - sdisalvo: handle error - const seedPhraseString = originalSeedPhrase.join(" "); - const entropy = Addresses.convertToEntropy(seedPhraseString); - await SecureStorage.set( - KeyStoreKeys.IDENTITY_ROOT_XPRV_KEY, - Addresses.bech32ToHexBip32Private( - Addresses.entropyToBip32NoPasscode(entropy) - ) - ); - await SecureStorage.set(KeyStoreKeys.IDENTITY_ENTROPY, entropy); - await SecureStorage.set(KeyStoreKeys.SIGNIFY_BRAN, seedPhraseStore.bran); - handleNavigate(); }; From 7c1b27c16346eccf99ed77cbeea136d23032e5cc Mon Sep 17 00:00:00 2001 From: Bao Hoang <142210850+bao-sotatek@users.noreply.github.com> Date: Tue, 4 Jun 2024 14:37:57 +0700 Subject: [PATCH 02/28] refactor(core): remove legacy connection detail items (#502) --- src/core/agent/agent.types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/core/agent/agent.types.ts b/src/core/agent/agent.types.ts index 7496731bd..47775c550 100644 --- a/src/core/agent/agent.types.ts +++ b/src/core/agent/agent.types.ts @@ -50,9 +50,6 @@ type ConnectionNoteDetails = { type ConnectionNoteProps = Pick; interface ConnectionDetails extends ConnectionShortDetails { - goalCode?: string; - handshakeProtocols?: string[]; - requestAttachments?: string[]; serviceEndpoints?: string[]; notes?: ConnectionNoteDetails[]; } From 8a86b07873c3fdf7c9b87f19d31238d241b48988 Mon Sep 17 00:00:00 2001 From: Patrick Nguyen Date: Tue, 4 Jun 2024 17:37:51 +0700 Subject: [PATCH 03/28] feat: hook peer connect UI to the core (#494) * feat: replace mock peerconnection with the real functions * refactor: update sign so it can sign arbitrary data * refactor: update sign so it can sign arbitrary data * update: small UI updates * update: small UI updates * refactor: rename css classes and event type * update: change css for string data signing input * refactor: remove label in incoming signing request since it is duplicated * feat: peer connect redux updates * feat: add unit tests for peer connection handlers * refactor: setConnectedWallet with the dApp address * refactor: update the peer connections list with a prototype connection --- package-lock.json | 8 +- package.json | 2 +- .../walletConnect/identityWalletConnect.ts | 4 +- .../cardano/walletConnect/peerConnection.ts | 99 +++++++++++++-- .../walletConnect/peerConnection.types.ts | 25 +++- src/locales/en/en.json | 19 +-- src/routes/backRoute/backRoute.test.ts | 4 +- src/routes/nextRoute/nextRoute.test.ts | 4 +- .../reducers/stateCache/stateCache.types.ts | 8 +- .../walletConnectionsCache.test.ts | 70 ++++------ .../walletConnectionsCache.ts | 22 ++-- .../walletConnectionsCache.types.ts | 16 +-- src/ui/App.test.tsx | 4 + src/ui/__fixtures__/signTransactionFix.ts | 56 +++++--- src/ui/__fixtures__/walletConnectionsFix.ts | 28 ++-- .../components/AppWrapper/AppWrapper.test.tsx | 102 +++++++++++++++ src/ui/components/AppWrapper/AppWrapper.tsx | 74 ++++++++++- src/ui/components/CardList/CardList.test.tsx | 6 +- src/ui/components/Scanner/Scanner.tsx | 6 +- .../ConfirmConnectModal.test.tsx | 14 +- .../ConfirmConnectModal.tsx | 12 +- .../ConnectWallet/ConnectWallet.test.tsx | 74 ++++++++--- .../ConnectWallet/ConnectWallet.tsx | 49 +++---- src/ui/pages/SidePage/SidePage.test.tsx | 6 +- src/ui/pages/SidePage/SidePage.tsx | 6 +- .../IncomingRequest/IncomingRequest.tsx | 4 + .../components/RequestComponent.test.tsx | 120 +++++++++++++++--- .../components/RequestComponent.tsx | 6 +- ...ansactionRequest.scss => SignRequest.scss} | 27 ++-- .../components/SignRequest.tsx | 97 ++++++++++++++ .../components/SignTransactionRequest.tsx | 112 ---------------- .../SignTransactionRequest.types.ts | 23 ---- .../WalletConnect/WalletConnect.test.tsx | 34 +++-- .../WalletConnect/WalletConnect.tsx | 23 ++-- .../WalletConnect/WalletConnect.types.ts | 4 +- .../WalletConnect/WalletConnectStageTwo.tsx | 21 ++- 36 files changed, 777 insertions(+), 412 deletions(-) rename src/ui/pages/SidePage/components/IncomingRequest/components/{SignTransactionRequest.scss => SignRequest.scss} (78%) create mode 100644 src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx delete mode 100644 src/ui/pages/SidePage/components/IncomingRequest/components/SignTransactionRequest.tsx delete mode 100644 src/ui/pages/SidePage/components/IncomingRequest/components/SignTransactionRequest.types.ts diff --git a/package-lock.json b/package-lock.json index 16002d7e1..e985a4bb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@capacitor/share": "^5.0.0", "@capacitor/splash-screen": "^5.0.0", "@capacitor/status-bar": "^5.0.7", - "@fabianbormann/cardano-peer-connect": "^1.2.16", + "@fabianbormann/cardano-peer-connect": "^1.2.17", "@ionic/react": "^7.5.4", "@ionic/react-router": "^7.5.4", "@ionic/storage": "^3.0.6", @@ -3137,9 +3137,9 @@ } }, "node_modules/@fabianbormann/cardano-peer-connect": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@fabianbormann/cardano-peer-connect/-/cardano-peer-connect-1.2.16.tgz", - "integrity": "sha512-PA79czqgdYhgzf0oxk3596bo+T0/iySq6Lv5uueTXXsu+awDff0LtOzfvUiOHyu7dqNA3+M6gHoJsmPg+/gP8w==", + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@fabianbormann/cardano-peer-connect/-/cardano-peer-connect-1.2.17.tgz", + "integrity": "sha512-i9CEhAWGg0Ud0bz84+bG2UOB5GNsb479AhfhRGCH5s+TqFUkkoKB2T1R+Yp4YcYGCUaaYUwHTtuFJwvtlrAcCQ==", "dependencies": { "@basementuniverse/marble-identicons": "^0.1.2", "@fabianbormann/meerkat": "^1.0.16", diff --git a/package.json b/package.json index 7a76ad48f..ef23a18bd 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@capacitor/share": "^5.0.0", "@capacitor/splash-screen": "^5.0.0", "@capacitor/status-bar": "^5.0.7", - "@fabianbormann/cardano-peer-connect": "^1.2.16", + "@fabianbormann/cardano-peer-connect": "^1.2.17", "@ionic/react": "^7.5.4", "@ionic/react-router": "^7.5.4", "@ionic/storage": "^3.0.6", diff --git a/src/core/cardano/walletConnect/identityWalletConnect.ts b/src/core/cardano/walletConnect/identityWalletConnect.ts index 4f114e487..c8c059a01 100644 --- a/src/core/cardano/walletConnect/identityWalletConnect.ts +++ b/src/core/cardano/walletConnect/identityWalletConnect.ts @@ -9,7 +9,7 @@ import { Signer } from "signify-ts"; import { Agent } from "../../agent/agent"; import { PeerConnectSigningEvent, - PeerConnectSigningEventTypes, + PeerConnectionEventTypes, PeerConnectionError, TxSignError, } from "./peerConnection.types"; @@ -68,7 +68,7 @@ class IdentityWalletConnect extends CardanoPeerConnect { approved = approvalStatus; }; this.eventService.emit({ - type: PeerConnectSigningEventTypes.PeerConnectSign, + type: PeerConnectionEventTypes.PeerConnectSign, payload: { identifier, payload, diff --git a/src/core/cardano/walletConnect/peerConnection.ts b/src/core/cardano/walletConnect/peerConnection.ts index 810fdd0f3..ac662cfa8 100644 --- a/src/core/cardano/walletConnect/peerConnection.ts +++ b/src/core/cardano/walletConnect/peerConnection.ts @@ -9,8 +9,12 @@ import { EventService } from "../../agent/services/eventService"; import { ExperimentalAPIFunctions, PeerConnectSigningEvent, - PeerConnectSigningEventTypes, + PeerConnectedEvent, + PeerConnectionEventTypes, + PeerDisconnectedEvent, } from "./peerConnection.types"; +import { Agent } from "../../agent/agent"; +import { PeerConnectionStorage } from "../../agent/records"; class PeerConnection { static readonly PEER_CONNECTION_START_PENDING = @@ -33,7 +37,6 @@ class PeerConnection { ]; private identityWalletConnect: IdentityWalletConnect | undefined; - private connected = false; private connectedDAppAdress = ""; private eventService = new EventService(); private static instance: PeerConnection; @@ -42,13 +45,33 @@ class PeerConnection { callback: (event: PeerConnectSigningEvent) => void ) { this.eventService.on( - PeerConnectSigningEventTypes.PeerConnectSign, + PeerConnectionEventTypes.PeerConnectSign, async (event: PeerConnectSigningEvent) => { callback(event); } ); } + onPeerConnectedStateChanged(callback: (event: PeerConnectedEvent) => void) { + this.eventService.on( + PeerConnectionEventTypes.PeerConnected, + async (event: PeerConnectedEvent) => { + callback(event); + } + ); + } + + onPeerDisconnectedStateChanged( + callback: (event: PeerDisconnectedEvent) => void + ) { + this.eventService.on( + PeerConnectionEventTypes.PeerDisconnected, + async (event: PeerDisconnectedEvent) => { + callback(event); + } + ); + } + static get peerConnection() { if (!this.instance) { this.instance = new PeerConnection(); @@ -80,17 +103,49 @@ class PeerConnection { this.eventService ); this.identityWalletConnect.setOnConnect( - (connectMessage: IConnectMessage) => { + async (connectMessage: IConnectMessage) => { if (!connectMessage.error) { - this.connected = true; - this.connectedDAppAdress = connectMessage.dApp.address; + const { name, url, address, icon } = connectMessage.dApp; + this.connectedDAppAdress = address; + let iconB64 = ICON_BASE64; + // Check if the icon is base64 + if ( + icon && + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/.test( + icon + ) + ) { + iconB64 = icon; + } + await Agent.agent.peerConnectionMetadataStorage.updatePeerConnectionMetadata( + address, + { + name, + selectedAid, + url, + iconB64: iconB64, + } + ); + this.eventService.emit({ + type: PeerConnectionEventTypes.PeerConnected, + payload: { + identifier: selectedAid, + dAppAddress: address, + }, + }); } } ); this.identityWalletConnect.setOnDisconnect( (disConnectMessage: IConnectMessage) => { - this.connected = false; + this.connectedDAppAdress = ""; + this.eventService.emit({ + type: PeerConnectionEventTypes.PeerDisconnected, + payload: { + dAppAddress: disConnectMessage.address as string, + }, + }); } ); @@ -102,12 +157,33 @@ class PeerConnection { ); } - connectWithDApp(dAppIdentifier: string) { + async connectWithDApp(dAppIdentifier: string) { if (this.identityWalletConnect === undefined) { throw new Error(PeerConnection.PEER_CONNECTION_START_PENDING); } - + const existingPeerConnection = + await Agent.agent.peerConnectionMetadataStorage + .getPeerConnectionMetadata(dAppIdentifier) + .catch((error) => { + if ( + error.message === + PeerConnectionStorage.PEER_CONNECTION_METADATA_RECORD_MISSING + ) { + return undefined; + } else { + throw error; + } + }); + if (!existingPeerConnection) { + await Agent.agent.peerConnectionMetadataStorage.createPeerConnectionMetadataRecord( + { + id: dAppIdentifier, + iconB64: ICON_BASE64, + } + ); + } const seed = this.identityWalletConnect.connect(dAppIdentifier); + SecureStorage.set(KeyStoreKeys.MEERKAT_SEED, seed); } @@ -117,11 +193,10 @@ class PeerConnection { } this.identityWalletConnect.disconnect(dAppIdentifier); - this.connected = false; } - isConnected() { - return this.connected; + getConnectedDAppAddress() { + return this.connectedDAppAdress; } } diff --git a/src/core/cardano/walletConnect/peerConnection.types.ts b/src/core/cardano/walletConnect/peerConnection.types.ts index b6b3e91e0..3953a28a3 100644 --- a/src/core/cardano/walletConnect/peerConnection.types.ts +++ b/src/core/cardano/walletConnect/peerConnection.types.ts @@ -11,12 +11,14 @@ interface ExperimentalAPIFunctions { ) => Promise; } -enum PeerConnectSigningEventTypes { +enum PeerConnectionEventTypes { PeerConnectSign = "PeerConnectSign", + PeerConnected = "PeerConnected", + PeerDisconnected = "PeerDisconnected", } interface PeerConnectSigningEvent extends BaseEventEmitter { - type: typeof PeerConnectSigningEventTypes.PeerConnectSign; + type: typeof PeerConnectionEventTypes.PeerConnectSign; payload: { identifier: string; payload: string; @@ -24,6 +26,21 @@ interface PeerConnectSigningEvent extends BaseEventEmitter { }; } +interface PeerConnectedEvent extends BaseEventEmitter { + type: typeof PeerConnectionEventTypes.PeerConnected; + payload: { + identifier: string; + dAppAddress: string; + }; +} + +interface PeerDisconnectedEvent extends BaseEventEmitter { + type: typeof PeerConnectionEventTypes.PeerDisconnected; + payload: { + dAppAddress: string; + }; +} + interface PeerConnectionError { code: number; info: string; @@ -38,9 +55,11 @@ export const TxSignError: { [key: string]: PeerConnectionError } = { TimeOut: { code: 3, info: "Time out" }, }; -export { PeerConnectSigningEventTypes }; +export { PeerConnectionEventTypes }; export type { ExperimentalAPIFunctions, PeerConnectSigningEvent, + PeerConnectedEvent, + PeerDisconnectedEvent, PeerConnectionError, }; diff --git a/src/locales/en/en.json b/src/locales/en/en.json index 2e8f55fad..9390a3ee3 100644 --- a/src/locales/en/en.json +++ b/src/locales/en/en.json @@ -1188,22 +1188,11 @@ "setupbiometricsuccess": "Biometrics setup successfully" }, "request": { - "signtransaction": { - "title": "Sign transaction", + "sign": { + "title": "Sign", + "identifier": "Identifier", "transaction": { - "address": "Address", - "data": "Data", - "action": "Action", - "actionText": "Action text", - "id": "ID", - "event": "Event", - "category": "Category", - "proposal": "Proposal", - "votedat": "Voted at", - "network": "Network", - "votingpower": "Voting power", - "slot": "Slot", - "uri": "URI" + "data": "Data" } }, "connection": { diff --git a/src/routes/backRoute/backRoute.test.ts b/src/routes/backRoute/backRoute.test.ts index 7185a3584..63f266f01 100644 --- a/src/routes/backRoute/backRoute.test.ts +++ b/src/routes/backRoute/backRoute.test.ts @@ -56,7 +56,7 @@ describe("getBackRoute", () => { walletConnectionsCache: { walletConnections: [], connectedWallet: null, - pendingConnection: null, + pendingDAppMeerKat: null, }, identifierViewTypeCacheCache: { viewType: null, @@ -177,7 +177,7 @@ describe("getPreviousRoute", () => { walletConnectionsCache: { walletConnections: [], connectedWallet: null, - pendingConnection: null, + pendingDAppMeerKat: null, }, identifierViewTypeCacheCache: { viewType: null, diff --git a/src/routes/nextRoute/nextRoute.test.ts b/src/routes/nextRoute/nextRoute.test.ts index 80112888e..3df333992 100644 --- a/src/routes/nextRoute/nextRoute.test.ts +++ b/src/routes/nextRoute/nextRoute.test.ts @@ -58,7 +58,7 @@ describe("NextRoute", () => { walletConnectionsCache: { walletConnections: [], connectedWallet: null, - pendingConnection: null, + pendingDAppMeerKat: null, }, identifierViewTypeCacheCache: { viewType: null, @@ -177,7 +177,7 @@ describe("getNextRoute", () => { walletConnectionsCache: { walletConnections: [], connectedWallet: null, - pendingConnection: null, + pendingDAppMeerKat: null, }, identifierViewTypeCacheCache: { viewType: null, diff --git a/src/store/reducers/stateCache/stateCache.types.ts b/src/store/reducers/stateCache/stateCache.types.ts index 9d64a1a26..00ee53a29 100644 --- a/src/store/reducers/stateCache/stateCache.types.ts +++ b/src/store/reducers/stateCache/stateCache.types.ts @@ -1,7 +1,8 @@ import { KeriaNotification } from "../../../core/agent/agent.types"; import { MultiSigIcpRequestDetails } from "../../../core/agent/services/identifier.types"; +import { PeerConnectSigningEvent } from "../../../core/cardano/walletConnect/peerConnection.types"; import { OperationType, ToastMsgType } from "../../../ui/globals/types"; -import { SignTransaction } from "../../../ui/pages/SidePage/components/IncomingRequest/components/SignTransactionRequest.types"; +import { ConnectionData } from "../walletConnectionsCache"; interface PayloadData { [key: string]: T; @@ -23,7 +24,7 @@ interface AuthenticationCacheProps { enum IncomingRequestType { CREDENTIAL_OFFER_RECEIVED = "credential-offer-received", MULTI_SIG_REQUEST_INCOMING = "multi-sig-request-incoming", - SIGN_TRANSACTION_REQUEST = "sign-transaction-request", + PEER_CONNECT_SIGN = "peer-connect-sign", } interface IncomingRequestProps { @@ -33,7 +34,8 @@ interface IncomingRequestProps { label?: string; event?: KeriaNotification; multisigIcpDetails?: MultiSigIcpRequestDetails; - signTransaction?: SignTransaction; + signTransaction?: PeerConnectSigningEvent; + peerConnection?: ConnectionData; } interface QueueProps { diff --git a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.test.ts b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.test.ts index dce0a584e..7e1ffc6bf 100644 --- a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.test.ts +++ b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.test.ts @@ -2,10 +2,10 @@ import { PayloadAction } from "@reduxjs/toolkit"; import { RootState } from "../../index"; import { getConnectedWallet, - getPendingConnection, + getPendingDAppMeerkat, getWalletConnectionsCache, setConnectedWallet, - setPendingConnections, + setPendingDAppMeerKat, setWalletConnectionsCache, walletConnectionsCacheSlice, } from "./walletConnectionsCache"; @@ -18,7 +18,7 @@ describe("walletConnectionsCacheSlice", () => { const initialState: WalletConnectState = { walletConnections: [], connectedWallet: null, - pendingConnection: null, + pendingDAppMeerKat: null, }; it("should return the initial state", () => { @@ -30,10 +30,10 @@ describe("walletConnectionsCacheSlice", () => { it("should handle setWalletConnectionsCache", () => { const connections: ConnectionData[] = [ { - id: 2, + id: "2", name: "Wallet name #2", - owner: "Yoroi", - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", + selectedAid: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRc", + url: "http://localhost:3001/", }, ]; const newState = walletConnectionsCacheSlice.reducer( @@ -44,29 +44,23 @@ describe("walletConnectionsCacheSlice", () => { }); it("should handle setConnectedWallet", () => { const connection: ConnectionData = { - id: 2, + id: "2", name: "Wallet name #2", - owner: "Yoroi", - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", + selectedAid: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRc", + url: "http://localhost:3001/", }; const newState = walletConnectionsCacheSlice.reducer( initialState, - setConnectedWallet(connection) + setConnectedWallet(connection.id) ); - expect(newState.connectedWallet).toEqual(connection); + expect(newState.connectedWallet).toEqual(connection.id); }); - it("should handle setPendingConnection", () => { - const connection: ConnectionData = { - id: 2, - name: "Wallet name #2", - owner: "Yoroi", - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", - }; + it("should handle setPendingDAppMeerKat", () => { const newState = walletConnectionsCacheSlice.reducer( initialState, - setPendingConnections(connection) + setPendingDAppMeerKat("pending-meerkat") ); - expect(newState.pendingConnection).toEqual(connection); + expect(newState.pendingDAppMeerKat).toEqual("pending-meerkat"); }); }); @@ -76,17 +70,17 @@ describe("Get wallet connections cache", () => { walletConnectionsCache: { walletConnections: [ { - id: 1, + id: "1", name: "Wallet name #1", - owner: "Nami", + selectedAid: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRd", image: "", - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", + url: "http://localhost:3001/", }, { - id: 2, + id: "2", name: "Wallet name #2", - owner: "Yoroi", - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", + selectedAid: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRc", + url: "http://localhost:3001/", }, ], }, @@ -100,13 +94,7 @@ describe("Get wallet connections cache", () => { it("should return connected wallet from RootState", () => { const state = { walletConnectionsCache: { - connectedWallet: { - id: 1, - name: "Wallet name #1", - owner: "Nami", - image: "", - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", - }, + connectedWallet: "1", }, } as RootState; const connectionCache = getConnectedWallet(state); @@ -114,21 +102,15 @@ describe("Get wallet connections cache", () => { state.walletConnectionsCache.connectedWallet ); }); - it("should return pending connection from RootState", () => { + it("should return pending DApp MeerKat from RootState", () => { const state = { walletConnectionsCache: { - pendingConnection: { - id: 1, - name: "Wallet name #1", - owner: "Nami", - image: "", - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", - }, + pendingDAppMeerKat: "pending-meerkat", }, } as RootState; - const connectionCache = getPendingConnection(state); - expect(connectionCache).toEqual( - state.walletConnectionsCache.pendingConnection + const pendingMeerKatCache = getPendingDAppMeerkat(state); + expect(pendingMeerKatCache).toEqual( + state.walletConnectionsCache.pendingDAppMeerKat ); }); }); diff --git a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.ts b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.ts index 01ae194e9..1526d8502 100644 --- a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.ts +++ b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.ts @@ -8,7 +8,7 @@ import { const initialState: WalletConnectState = { walletConnections: [], connectedWallet: null, - pendingConnection: null, + pendingDAppMeerKat: null, }; const walletConnectionsCacheSlice = createSlice({ name: "walletConnectionsCache", @@ -20,17 +20,11 @@ const walletConnectionsCacheSlice = createSlice({ ) => { state.walletConnections = action.payload; }, - setConnectedWallet: ( - state, - action: PayloadAction - ) => { + setConnectedWallet: (state, action: PayloadAction) => { state.connectedWallet = action.payload; }, - setPendingConnections: ( - state, - action: PayloadAction - ) => { - state.pendingConnection = action.payload; + setPendingDAppMeerKat: (state, action: PayloadAction) => { + state.pendingDAppMeerKat = action.payload; }, }, }); @@ -40,7 +34,7 @@ export { initialState, walletConnectionsCacheSlice }; export const { setWalletConnectionsCache, setConnectedWallet, - setPendingConnections, + setPendingDAppMeerKat, } = walletConnectionsCacheSlice.actions; const getWalletConnectionsCache = (state: RootState) => @@ -49,7 +43,7 @@ const getWalletConnectionsCache = (state: RootState) => const getConnectedWallet = (state: RootState) => state.walletConnectionsCache.connectedWallet; -const getPendingConnection = (state: RootState) => - state.walletConnectionsCache.pendingConnection; +const getPendingDAppMeerkat = (state: RootState) => + state.walletConnectionsCache.pendingDAppMeerKat; -export { getWalletConnectionsCache, getConnectedWallet, getPendingConnection }; +export { getWalletConnectionsCache, getConnectedWallet, getPendingDAppMeerkat }; diff --git a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts index 826587ac6..c0adba811 100644 --- a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts +++ b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts @@ -1,16 +1,16 @@ -// TODO: mock data type for connect wallet ui. Need update after core function completed. interface ConnectionData { - id: number | string; - name: string; - owner: string; - image?: string; - url: string; + id: string; + name?: string; + url?: string; + createdAt?: Date; + iconB64?: string; + selectedAid?: string; } interface WalletConnectState { walletConnections: ConnectionData[]; - connectedWallet: ConnectionData | null; - pendingConnection: ConnectionData | null; + connectedWallet: string | null; + pendingDAppMeerKat: string | null; } export type { ConnectionData, WalletConnectState }; diff --git a/src/ui/App.test.tsx b/src/ui/App.test.tsx index 5fbcb9075..d93f55319 100644 --- a/src/ui/App.test.tsx +++ b/src/ui/App.test.tsx @@ -52,6 +52,10 @@ jest.mock("../core/agent/agent", () => ({ onNotificationStateChanged: jest.fn(), }, onKeriaStatusStateChanged: jest.fn(), + peerConnectionMetadataStorage: { + getAllPeerConnectionMetadata: jest.fn(), + getPeerConnectionMetadata: jest.fn(), + }, basicStorage: { findById: jest.fn(), }, diff --git a/src/ui/__fixtures__/signTransactionFix.ts b/src/ui/__fixtures__/signTransactionFix.ts index 8f3c7fd0d..c6c356eb1 100644 --- a/src/ui/__fixtures__/signTransactionFix.ts +++ b/src/ui/__fixtures__/signTransactionFix.ts @@ -1,22 +1,42 @@ -import { SignTransaction } from "../pages/SidePage/components/IncomingRequest/components/SignTransactionRequest.types"; +import { + PeerConnectSigningEvent, + PeerConnectionEventTypes, +} from "../../core/cardano/walletConnect/peerConnection.types"; -const signTransactionFix: SignTransaction = { - action: "CAST_VOTE", - actionText: "Cast Vote", - data: { - id: "2658fb7d-cd12-48c3-bc95-23e73616b79f", - address: "stake_test1uzpq2pktpnj54e64kfgjkm8nrptdwfj7s7fvhp40e98qsusd9z7ek", - event: "CF_TEST_EVENT_01", - category: "CHANGE_SOMETHING", - proposal: "YES", - network: "PREPROD", - votedAt: "40262406", - votingPower: "10444555666", +const signTransactionFix: PeerConnectSigningEvent = { + type: PeerConnectionEventTypes.PeerConnectSign, + payload: { + identifier: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRc", + payload: "Hello", + approvalCallback: (approvalStatus: boolean) => approvalStatus, }, - slot: "40262407", - uri: "https://evoting.cardano.org/voltaire", - ownerUrl: "https://voting.summit.cardano.org", - eventName: "Cardano Summit Voting Awards", }; -export { signTransactionFix }; +const signObjectFix: PeerConnectSigningEvent = { + type: PeerConnectionEventTypes.PeerConnectSign, + payload: { + identifier: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRc", + payload: JSON.stringify({ + action: "CAST_VOTE", + actionText: "Cast Vote", + data: { + id: "2658fb7d-cd12-48c3-bc95-23e73616b79f", + address: + "stake_test1uzpq2pktpnj54e64kfgjkm8nrptdwfj7s7fvhp40e98qsusd9z7ek", + event: "CF_TEST_EVENT_01", + category: "CHANGE_SOMETHING", + proposal: "YES", + network: "PREPROD", + votedAt: "40262406", + votingPower: "10444555666", + }, + slot: "40262407", + uri: "https://evoting.cardano.org/voltaire", + ownerUrl: "https://voting.summit.cardano.org", + eventName: "Cardano Summit Voting Awards", + }), + approvalCallback: (approvalStatus: boolean) => approvalStatus, + }, +}; + +export { signTransactionFix, signObjectFix }; diff --git a/src/ui/__fixtures__/walletConnectionsFix.ts b/src/ui/__fixtures__/walletConnectionsFix.ts index 1175ce821..d87ec1a2c 100644 --- a/src/ui/__fixtures__/walletConnectionsFix.ts +++ b/src/ui/__fixtures__/walletConnectionsFix.ts @@ -3,30 +3,30 @@ import KeriLogo from "../assets/images/KeriGeneric.jpg"; const walletConnectionsFix: ConnectionData[] = [ { - id: 1, + id: "1", name: "Wallet name #1", - owner: "Nami", - image: KeriLogo, - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", + selectedAid: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRd", + iconB64: KeriLogo, + url: "http://localhost:3001/", }, { - id: 2, + id: "2", name: "Wallet name #2", - owner: "Yoroi", - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", + selectedAid: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRc", + url: "http://localhost:3002/", }, { - id: 3, + id: "3", name: "Wallet name #3", - owner: "Flint", - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", - image: KeriLogo, + selectedAid: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRe", + url: "http://localhost:3003/", + iconB64: KeriLogo, }, { - id: 4, + id: "4", name: "Wallet name #4", - owner: "Yume", - url: "ED4KeyyTKFj-72B008OTGgDCrFo6y7B2B73kfyzu5Inb", + selectedAid: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRf", + url: "http://localhost:3004/", }, ]; diff --git a/src/ui/components/AppWrapper/AppWrapper.test.tsx b/src/ui/components/AppWrapper/AppWrapper.test.tsx index 203157383..10f2a3a80 100644 --- a/src/ui/components/AppWrapper/AppWrapper.test.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.test.tsx @@ -5,6 +5,9 @@ import { acdcChangeHandler, connectionStateChangedHandler, keriaNotificationsChangeHandler, + peerConnectRequestSignChangeHandler, + peerConnectedChangeHandler, + peerDisconnectedChangeHandler, } from "./AppWrapper"; import { store } from "../../../store"; import { Agent } from "../../../core/agent/agent"; @@ -31,6 +34,17 @@ import { CredentialShortDetails, CredentialStatus, } from "../../../core/agent/services/credentialService.types"; +import { + PeerConnectSigningEvent, + PeerConnectedEvent, + PeerConnectionEventTypes, + PeerDisconnectedEvent, +} from "../../../core/cardano/walletConnect/peerConnection.types"; +import { + ConnectionData, + setConnectedWallet, + setWalletConnectionsCache, +} from "../../../store/reducers/walletConnectionsCache"; jest.mock("../../../core/agent/agent", () => ({ Agent: { @@ -78,6 +92,10 @@ jest.mock("../../../core/agent/agent", () => ({ }, getKeriaOnlineStatus: jest.fn(), onKeriaStatusStateChanged: jest.fn(), + peerConnectionMetadataStorage: { + getAllPeerConnectionMetadata: jest.fn(), + getPeerConnectionMetadata: jest.fn(), + }, basicStorage: { findById: jest.fn(), save: jest.fn(), @@ -123,6 +141,40 @@ const connectionShortDetailsMock = { logo: "png", } as ConnectionShortDetails; +const peerConnectedEventMock = { + type: PeerConnectionEventTypes.PeerConnected, + payload: { + identifier: "identifier", + dAppAddress: "dApp-address", + }, +} as PeerConnectedEvent; + +const peerDisconnectedEventMock = { + type: PeerConnectionEventTypes.PeerDisconnected, + payload: { + identifier: "identifier", + dAppAddress: "dApp-address", + }, +} as PeerDisconnectedEvent; + +const peerSignRequestEventMock = { + type: PeerConnectionEventTypes.PeerConnectSign, + payload: { + identifier: "identifier", + approvalCallback: function () { + return; + }, + payload: "Hello", + }, +} as PeerConnectSigningEvent; + +const peerConnectionMock: ConnectionData = { + id: "dApp-address", + name: "dApp-name", + iconB64: "icon", + selectedAid: "identifier", + url: "http://localhost:3000", +}; const dispatch = jest.fn(); describe("AppWrapper handler", () => { describe("Connection state changed handler", () => { @@ -245,4 +297,54 @@ describe("AppWrapper handler", () => { ); }); }); + + describe("Peer connection states changed handler", () => { + test("handle peer connected event", async () => { + Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata = jest + .fn() + .mockResolvedValue(peerConnectionMock); + Agent.agent.peerConnectionMetadataStorage.getAllPeerConnectionMetadata = + jest.fn().mockResolvedValue([peerConnectionMock]); + await peerConnectedChangeHandler(peerConnectedEventMock, dispatch); + expect(dispatch).toBeCalledWith( + setConnectedWallet(peerConnectionMock.id) + ); + expect(dispatch).toBeCalledWith( + setWalletConnectionsCache([peerConnectionMock]) + ); + expect(dispatch).toBeCalledWith( + setToastMsg(ToastMsgType.CONNECT_WALLET_SUCCESS) + ); + }); + + test("handle peer disconnected event", async () => { + await peerDisconnectedChangeHandler( + peerDisconnectedEventMock, + peerConnectionMock.id, + dispatch + ); + expect(dispatch).toBeCalledWith(setConnectedWallet(null)); + expect(dispatch).toBeCalledWith( + setToastMsg(ToastMsgType.DISCONNECT_WALLET_SUCCESS) + ); + }); + + test("handle peer sign request event", async () => { + Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata = jest + .fn() + .mockResolvedValue(peerConnectionMock); + await peerConnectRequestSignChangeHandler( + peerSignRequestEventMock, + dispatch + ); + expect(dispatch).toBeCalledWith( + setQueueIncomingRequest({ + id: "peer-connect-signing", + signTransaction: peerSignRequestEventMock, + peerConnection: peerConnectionMock, + type: IncomingRequestType.PEER_CONNECT_SIGN, + }) + ); + }); + }); }); diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index 64e762f0f..8bf7debf5 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -40,14 +40,22 @@ import { FavouriteIdentifier } from "../../../store/reducers/identifiersCache/id import "./AppWrapper.scss"; import { ConfigurationService } from "../../../core/configuration"; import { useActivityTimer } from "./hooks/useActivityTimer"; -import { setWalletConnectionsCache } from "../../../store/reducers/walletConnectionsCache"; -import { walletConnectionsFix } from "../../__fixtures__/walletConnectionsFix"; +import { + getConnectedWallet, + setConnectedWallet, + setWalletConnectionsCache, +} from "../../../store/reducers/walletConnectionsCache"; import { PeerConnection } from "../../../core/cardano/walletConnect/peerConnection"; -import { PeerConnectSigningEvent } from "../../../core/cardano/walletConnect/peerConnection.types"; +import { + PeerConnectSigningEvent, + PeerConnectedEvent, + PeerDisconnectedEvent, +} from "../../../core/cardano/walletConnect/peerConnection.types"; import { MultiSigService } from "../../../core/agent/services/multiSigService"; import { setViewTypeCache } from "../../../store/reducers/identifierViewTypeCache"; import { CardListViewType } from "../SwitchCardView"; import { setEnableBiometryCache } from "../../../store/reducers/biometryCache"; +import { i18n } from "../../../i18n"; const connectionStateChangedHandler = async ( event: ConnectionStateChangedEvent, @@ -137,13 +145,49 @@ const peerConnectRequestSignChangeHandler = async ( event: PeerConnectSigningEvent, dispatch: ReturnType ) => { - //TODO: Handle logic for the accept/decline sing request + const connectedDAppAddress = + PeerConnection.peerConnection.getConnectedDAppAddress(); + const peerConnection = + await Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata( + connectedDAppAddress + ); + dispatch( + setQueueIncomingRequest({ + id: "peer-connect-signing", + signTransaction: event, + peerConnection, + type: IncomingRequestType.PEER_CONNECT_SIGN, + }) + ); +}; + +const peerConnectedChangeHandler = async ( + event: PeerConnectedEvent, + dispatch: ReturnType +) => { + const existingConnections = + await Agent.agent.peerConnectionMetadataStorage.getAllPeerConnectionMetadata(); + dispatch(setWalletConnectionsCache(existingConnections)); + dispatch(setConnectedWallet(event.payload.dAppAddress)); + dispatch(setToastMsg(ToastMsgType.CONNECT_WALLET_SUCCESS)); +}; + +const peerDisconnectedChangeHandler = async ( + event: PeerDisconnectedEvent, + connectedMeerKat: string, + dispatch: ReturnType +) => { + if (connectedMeerKat === event.payload.dAppAddress) { + dispatch(setConnectedWallet(null)); + dispatch(setToastMsg(ToastMsgType.DISCONNECT_WALLET_SUCCESS)); + } }; const AppWrapper = (props: { children: ReactNode }) => { const dispatch = useAppDispatch(); const authentication = useAppSelector(getAuthentication); const operation = useAppSelector(getCurrentOperation); + const connectedWallet = useAppSelector(getConnectedWallet); const [isOnline, setIsOnline] = useState(false); const [isMessagesHandled, setIsMessagesHandled] = useState(false); useActivityTimer(); @@ -203,12 +247,13 @@ const AppWrapper = (props: { children: ReactNode }) => { const credentials = await Agent.agent.credentials.getCredentials(); const storedIdentifiers = await Agent.agent.identifiers.getIdentifiers(); + const storedPeerConnections = + await Agent.agent.peerConnectionMetadataStorage.getAllPeerConnectionMetadata(); dispatch(setIdentifiersCache(storedIdentifiers)); dispatch(setCredsCache(credentials)); dispatch(setConnectionsCache(connectionsDetails)); - // TODO: Need update after core function completed. - dispatch(setWalletConnectionsCache(walletConnectionsFix)); + dispatch(setWalletConnectionsCache(storedPeerConnections)); }; const loadCacheBasicStorage = async () => { @@ -321,6 +366,20 @@ const AppWrapper = (props: { children: ReactNode }) => { return peerConnectRequestSignChangeHandler(event, dispatch); } ); + PeerConnection.peerConnection.onPeerConnectedStateChanged(async (event) => { + return peerConnectedChangeHandler(event, dispatch); + }); + PeerConnection.peerConnection.onPeerDisconnectedStateChanged( + async (event) => { + if (connectedWallet) { + return peerDisconnectedChangeHandler( + event, + connectedWallet, + dispatch + ); + } + } + ); dispatch(setInitialized(true)); }; @@ -332,4 +391,7 @@ export { connectionStateChangedHandler, acdcChangeHandler, keriaNotificationsChangeHandler, + peerConnectedChangeHandler, + peerDisconnectedChangeHandler, + peerConnectRequestSignChangeHandler, }; diff --git a/src/ui/components/CardList/CardList.test.tsx b/src/ui/components/CardList/CardList.test.tsx index e56f4ee4c..738399c71 100644 --- a/src/ui/components/CardList/CardList.test.tsx +++ b/src/ui/components/CardList/CardList.test.tsx @@ -5,7 +5,7 @@ import { walletConnectionsFix } from "../../__fixtures__/walletConnectionsFix"; const displayConnection = walletConnectionsFix.map((connection, index) => ({ id: connection.id, title: connection.name, - subtitle: index % 2 === 0 ? connection.owner : undefined, + subtitle: index % 2 === 0 ? connection.selectedAid : undefined, image: index % 2 === 0 ? "mock-image-link" : undefined, data: connection, })); @@ -16,7 +16,7 @@ describe("Card list", () => { const { getByTestId, queryByTestId, getAllByText, getAllByTestId } = render( } onRenderEndSlot={() => End slot} @@ -44,7 +44,7 @@ describe("Card list", () => { const { queryByTestId, getByTestId } = render( ); diff --git a/src/ui/components/Scanner/Scanner.tsx b/src/ui/components/Scanner/Scanner.tsx index 5c78bb178..561f6d3e5 100644 --- a/src/ui/components/Scanner/Scanner.tsx +++ b/src/ui/components/Scanner/Scanner.tsx @@ -35,8 +35,7 @@ import { MultiSigGroup } from "../../../store/reducers/identifiersCache/identifi import { PageFooter } from "../PageFooter"; import { CustomInput } from "../CustomInput"; import { OptionModal } from "../OptionsModal"; -import { setPendingConnections } from "../../../store/reducers/walletConnectionsCache"; -import { walletConnectionsFix } from "../../__fixtures__/walletConnectionsFix"; +import { setPendingDAppMeerKat } from "../../../store/reducers/walletConnectionsCache"; import { CreateIdentifier } from "../CreateIdentifier"; const Scanner = forwardRef( @@ -94,10 +93,9 @@ const Scanner = forwardRef( })); const handleConnectWallet = (id: string) => { - // TODO: Handle connect wallet using the id handleReset && handleReset(); dispatch(setToastMsg(ToastMsgType.PEER_ID_SUCCESS)); - dispatch(setPendingConnections(walletConnectionsFix[0])); + dispatch(setPendingDAppMeerKat(id)); }; const updateConnections = async (groupId: string) => { diff --git a/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.test.tsx b/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.test.tsx index 127f4a010..e5cada087 100644 --- a/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.test.tsx +++ b/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.test.tsx @@ -58,7 +58,7 @@ describe("Confirm connect modal", () => { onDeleteConnection={deleteFn} connectionData={{ ...walletConnectionsFix[0], - image: "imagelink", + iconB64: "imagelink", }} isConnectModal={true} /> @@ -67,13 +67,15 @@ describe("Confirm connect modal", () => { expect(getByTestId("wallet-connection-logo")).toBeVisible(); - expect(getByText(walletConnectionsFix[0].name)).toBeVisible(); - expect(getByText(walletConnectionsFix[0].owner)).toBeVisible(); + expect(getByText(walletConnectionsFix[0].name as string)).toBeVisible(); + expect( + getByText(walletConnectionsFix[0].selectedAid as string) + ).toBeVisible(); const ellipsisLink = - walletConnectionsFix[0].url.substring(0, 5) + + (walletConnectionsFix[0].url as string).substring(0, 5) + "..." + - walletConnectionsFix[0].url.slice(-5); + (walletConnectionsFix[0].url as string).slice(-5); expect(getByText(ellipsisLink)).toBeVisible(); @@ -113,7 +115,7 @@ describe("Confirm connect modal", () => { onDeleteConnection={deleteFn} connectionData={{ ...walletConnectionsFix[0], - image: undefined, + iconB64: undefined, }} isConnectModal={false} /> diff --git a/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.tsx b/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.tsx index e096c411f..dedd6ee73 100644 --- a/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.tsx +++ b/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.tsx @@ -20,9 +20,9 @@ const ConfirmConnectModal = ({ }: ConfirmConnectModalProps) => { const dispatch = useAppDispatch(); - const cardImg = connectionData?.image ? ( + const cardImg = connectionData?.iconB64 ? ( {connectionData.name} { @@ -85,11 +87,11 @@ const ConfirmConnectModal = ({ > {cardImg}

{connectionData?.name}

-

{connectionData?.owner}

+

{connectionData?.selectedAid}

{ if (!connectionData) return; - writeToClipboard(connectionData.url); + writeToClipboard(connectionData.url as string); dispatch(setToastMsg(ToastMsgType.COPIED_TO_CLIPBOARD)); }} className="confirm-modal-id" diff --git a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx index 9edd9766b..e2f063c67 100644 --- a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx +++ b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx @@ -12,6 +12,27 @@ import { } from "../../../../../store/reducers/stateCache"; import { OperationType, ToastMsgType } from "../../../../globals/types"; import { identifierFix } from "../../../../__fixtures__/identifierFix"; +import { PeerConnection } from "../../../../../core/cardano/walletConnect/peerConnection"; +import { setPendingDAppMeerKat } from "../../../../../store/reducers/walletConnectionsCache"; + +jest.mock("../../../../../core/agent/agent", () => ({ + Agent: { + agent: { + peerConnectionMetadataStorage: { + getAllPeerConnectionMetadata: jest.fn(), + deletePeerConnectionMetadataRecord: jest.fn(), + }, + }, + }, +})); + +jest.mock("../../../../../core/cardano/walletConnect/peerConnection", () => ({ + PeerConnection: { + peerConnection: { + disconnectDApp: jest.fn(), + }, + }, +})); jest.mock("@ionic/react", () => ({ ...jest.requireActual("@ionic/react"), @@ -47,7 +68,7 @@ const initialState = { }, walletConnectionsCache: { walletConnections: [...walletConnectionsFix], - connectedWallet: walletConnectionsFix[1], + connectedWallet: walletConnectionsFix[1].id, }, identifiersCache: { identifiers: [...identifierFix], @@ -222,14 +243,14 @@ describe("Wallet connect", () => { EN_TRANSLATIONS.menu.tab.items.connectwallet.connectionhistory.title ) ).toBeVisible(); - expect(getByText(walletConnectionsFix[0].name)).toBeVisible(); - expect(getByText(walletConnectionsFix[0].owner)).toBeVisible(); - expect(getByText(walletConnectionsFix[1].name)).toBeVisible(); - expect(getByText(walletConnectionsFix[1].owner)).toBeVisible(); - expect(getByText(walletConnectionsFix[2].name)).toBeVisible(); - expect(getByText(walletConnectionsFix[2].owner)).toBeVisible(); - expect(getByText(walletConnectionsFix[3].name)).toBeVisible(); - expect(getByText(walletConnectionsFix[3].owner)).toBeVisible(); + expect(getByText(walletConnectionsFix[0].name as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[0].url as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[1].name as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[1].url as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[2].name as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[2].url as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[3].name as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[3].url as string)).toBeVisible(); expect(getByTestId("connected-wallet-check-mark")).toBeVisible(); }); @@ -245,14 +266,14 @@ describe("Wallet connect", () => { EN_TRANSLATIONS.menu.tab.items.connectwallet.connectionhistory.title ) ).toBeVisible(); - expect(getByText(walletConnectionsFix[0].name)).toBeVisible(); - expect(getByText(walletConnectionsFix[0].owner)).toBeVisible(); - expect(getByText(walletConnectionsFix[1].name)).toBeVisible(); - expect(getByText(walletConnectionsFix[1].owner)).toBeVisible(); - expect(getByText(walletConnectionsFix[2].name)).toBeVisible(); - expect(getByText(walletConnectionsFix[2].owner)).toBeVisible(); - expect(getByText(walletConnectionsFix[3].name)).toBeVisible(); - expect(getByText(walletConnectionsFix[3].owner)).toBeVisible(); + expect(getByText(walletConnectionsFix[0].name as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[0].url as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[1].name as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[1].url as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[2].name as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[2].url as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[3].name as string)).toBeVisible(); + expect(getByText(walletConnectionsFix[3].url as string)).toBeVisible(); expect(getByTestId("connected-wallet-check-mark")).toBeVisible(); }); @@ -367,7 +388,24 @@ describe("Wallet connect", () => { await waitFor(() => { expect(dispatchMock).toBeCalledWith( - setToastMsg(ToastMsgType.CONNECT_WALLET_SUCCESS) + setPendingDAppMeerKat(walletConnectionsFix[0].id) + ); + }); + + act(() => { + fireEvent.click(getByTestId(`card-item-${walletConnectionsFix[1].id}`)); + }); + + await waitFor(() => { + expect(getByTestId("confirm-connect-btn")).toBeVisible(); + }); + + act(() => { + fireEvent.click(getByTestId("confirm-connect-btn")); + }); + await waitFor(() => { + expect(PeerConnection.peerConnection.disconnectDApp).toBeCalledWith( + walletConnectionsFix[1].id ); }); }); diff --git a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx index 7f2526c15..4218dd103 100644 --- a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx +++ b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx @@ -21,7 +21,8 @@ import { ConnectionData, getConnectedWallet, getWalletConnectionsCache, - setConnectedWallet, + setPendingDAppMeerKat, + setWalletConnectionsCache, } from "../../../../../store/reducers/walletConnectionsCache"; import { Alert } from "../../../../components/Alert"; import { CardItem, CardList } from "../../../../components/CardList"; @@ -36,6 +37,8 @@ import { ActionType, ConnectWalletOptionRef, } from "./ConnectWallet.types"; +import { Agent } from "../../../../../core/agent/agent"; +import { PeerConnection } from "../../../../../core/cardano/walletConnect/peerConnection"; const ConnectWallet = forwardRef( (props, ref) => { @@ -64,12 +67,13 @@ const ConnectWallet = forwardRef( const displayConnection = useMemo((): CardItem[] => { return connections.map((connection) => ({ id: connection.id, - title: connection.name, - subtitle: connection.owner, - image: connection.image, + title: connection.name as string, + url: connection.url, + subtitle: connection.url, + image: connection.iconB64, data: connection, })); - }, []); + }, [connections]); useImperativeHandle(ref, () => ({ openConnectWallet: handleScanQR, @@ -115,31 +119,30 @@ const ConnectWallet = forwardRef( handleOpenVerify(); }; - const handleDeleteConnection = (data: ConnectionData) => { + const handleDeleteConnection = async (data: ConnectionData) => { actionInfo.current = { type: ActionType.None, }; + await Agent.agent.peerConnectionMetadataStorage.deletePeerConnectionMetadataRecord( + data.id + ); - // TODO: Implement delete wallet connection logic - + dispatch( + setWalletConnectionsCache( + connections.filter((connection) => connection.id !== data.id) + ) + ); dispatch(setToastMsg(ToastMsgType.WALLET_CONNECTION_DELETED)); }; const handleConnectWallet = () => { if (!actionInfo.current.data) return; - - // TODO: Implement logic connect/disconnect wallet - - const isConnectedItem = - actionInfo.current.data.id === connectedWallet?.id; - dispatch( - setConnectedWallet(!isConnectedItem ? actionInfo.current.data : null) - ); - - const toast = !isConnectedItem - ? ToastMsgType.CONNECT_WALLET_SUCCESS - : ToastMsgType.DISCONNECT_WALLET_SUCCESS; - dispatch(setToastMsg(toast)); + const isConnectedItem = actionInfo.current.data.id === connectedWallet; + if (isConnectedItem) { + PeerConnection.peerConnection.disconnectDApp(connectedWallet); + } else { + dispatch(setPendingDAppMeerKat(actionInfo.current.data.id)); + } }; const handleAfterVerify = () => { @@ -202,7 +205,7 @@ const ConnectWallet = forwardRef( ); }} onRenderEndSlot={(data) => { - if (data.id !== connectedWallet?.id) return null; + if (data.id !== connectedWallet) return null; return ( ( )}
setOpenConfirmConnectModal(false)} onConfirm={handleConnectWallet} diff --git a/src/ui/pages/SidePage/SidePage.test.tsx b/src/ui/pages/SidePage/SidePage.test.tsx index d7e509bc3..4ba791932 100644 --- a/src/ui/pages/SidePage/SidePage.test.tsx +++ b/src/ui/pages/SidePage/SidePage.test.tsx @@ -31,7 +31,7 @@ describe("Side Page: wallet connect", () => { identifiers: [...identifierFix], }, walletConnectionsCache: { - pendingConnection: walletConnectionsFix[0], + pendingDAppMeerKat: "pending-meerkat", }, }; @@ -111,9 +111,7 @@ describe("Side Page: incoming request", () => { identifiersCache: { identifiers: [...identifierFix], }, - walletConnectionsCache: { - pendingConnection: walletConnectionsFix[0], - }, + walletConnectionsCache: {}, }; const mockStore = configureStore(); diff --git a/src/ui/pages/SidePage/SidePage.tsx b/src/ui/pages/SidePage/SidePage.tsx index b1e442ad4..e53a4988f 100644 --- a/src/ui/pages/SidePage/SidePage.tsx +++ b/src/ui/pages/SidePage/SidePage.tsx @@ -5,7 +5,7 @@ import { setPauseQueueIncomingRequest, } from "../../../store/reducers/stateCache"; import { useAppDispatch, useAppSelector } from "../../../store/hooks"; -import { getPendingConnection } from "../../../store/reducers/walletConnectionsCache"; +import { getPendingDAppMeerkat } from "../../../store/reducers/walletConnectionsCache"; import { IncomingRequest } from "./components/IncomingRequest"; import { WalletConnect } from "./components/WalletConnect"; @@ -15,11 +15,11 @@ const SidePage = () => { const pauseIncommingRequestByConnection = useRef(false); const queueIncomingRequest = useAppSelector(getQueueIncomingRequest); - const pendingConnection = useAppSelector(getPendingConnection); + const pendingDAppMeerkat = useAppSelector(getPendingDAppMeerkat); const canOpenIncomingRequest = queueIncomingRequest.queues.length > 0 && !queueIncomingRequest.isPaused; - const canOpenPendingWalletConnection = !!pendingConnection; + const canOpenPendingWalletConnection = !!pendingDAppMeerkat; useEffect(() => { if (canOpenIncomingRequest) return; diff --git a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx index 89c007473..0aca4bf4b 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx @@ -67,6 +67,8 @@ const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { await Agent.agent.signifyNotifications.deleteNotificationRecordById( incomingRequest.id ); + } else if (incomingRequest.type === IncomingRequestType.PEER_CONNECT_SIGN) { + incomingRequest.signTransaction?.payload.approvalCallback(false); } handleReset(); }; @@ -77,6 +79,8 @@ const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { incomingRequest.type === IncomingRequestType.CREDENTIAL_OFFER_RECEIVED ) { Agent.agent.ipexCommunications.acceptAcdc(incomingRequest.id); + } else if (incomingRequest.type === IncomingRequestType.PEER_CONNECT_SIGN) { + incomingRequest.signTransaction?.payload.approvalCallback(true); } setTimeout(() => { handleReset(); diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.test.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.test.tsx index 078d97fd6..3ebfbae68 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.test.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.test.tsx @@ -8,7 +8,10 @@ import { store } from "../../../../../../store"; import { IncomingRequestType } from "../../../../../../store/reducers/stateCache/stateCache.types"; import { connectionsFix } from "../../../../../__fixtures__/connectionsFix"; import { filteredIdentifierFix } from "../../../../../__fixtures__/filteredIdentifierFix"; -import { signTransactionFix } from "../../../../../__fixtures__/signTransactionFix"; +import { + signTransactionFix, + signObjectFix, +} from "../../../../../__fixtures__/signTransactionFix"; import { RequestComponent } from "./RequestComponent"; setupIonicReact(); mockIonicReact(); @@ -17,7 +20,15 @@ jest.mock("@ionic/react", () => ({ ...jest.requireActual("@ionic/react"), IonAlert: ({ children }: { children: any }) => children, })); - +jest.mock("../../../../../../core/agent/agent", () => ({ + Agent: { + agent: { + peerConnectionMetadataStorage: { + getPeerConnectionMetadata: jest.fn(), + }, + }, + }, +})); describe("Multi-Sig request", () => { const mockStore = configureStore(); const dispatchMock = jest.fn(); @@ -191,8 +202,9 @@ describe("Sign request", () => { const requestData = { id: "abc123456", label: "Cardano", - type: IncomingRequestType.SIGN_TRANSACTION_REQUEST, + type: IncomingRequestType.PEER_CONNECT_SIGN, signTransaction: signTransactionFix, + peerConnection: { id: "id", name: "DApp" }, }; const initiateAnimation = false; @@ -213,30 +225,96 @@ describe("Sign request", () => { handleAccept={handleAccept} handleCancel={handleCancel} handleIgnore={handleIgnore} - incomingRequestType={IncomingRequestType.SIGN_TRANSACTION_REQUEST} + incomingRequestType={IncomingRequestType.PEER_CONNECT_SIGN} /> ); + expect(getByText(requestData.peerConnection?.name)).toBeVisible(); + expect( + getByText(requestData.signTransaction.payload.payload) + ).toBeVisible(); expect( - getByText(EN_TRANSLATIONS.request.signtransaction.title) + getByText(requestData.signTransaction.payload.identifier) ).toBeVisible(); + }); + + test("Display fallback image when provider logo is empty: BALLOT_TRANSACTION_REQUEST", async () => { + const testData = { + ...requestData, + logo: "", + }; + + const { getByTestId } = render( + + + + ); - expect(getByText(requestData.label)).toBeVisible(); - expect(getByText(signTransactionFix.action)).toBeVisible(); - expect(getByText(signTransactionFix.actionText)).toBeVisible(); - expect(getByText(signTransactionFix.data.id)).toBeVisible(); - expect(getByText(signTransactionFix.data.category)).toBeVisible(); - expect(getByText(signTransactionFix.data.event)).toBeVisible(); - expect(getByText(signTransactionFix.data.network)).toBeVisible(); - expect(getByText(signTransactionFix.data.proposal)).toBeVisible(); - expect(getByText(signTransactionFix.data.votedAt)).toBeVisible(); - expect(getByText(signTransactionFix.data.votingPower)).toBeVisible(); - expect(getByText(signTransactionFix.eventName)).toBeVisible(); - expect(getByText(signTransactionFix.ownerUrl)).toBeVisible(); - expect(getByText(signTransactionFix.slot)).toBeVisible(); - expect(getByText(signTransactionFix.uri)).toBeVisible(); - expect(getAllByText(signTransactionFix.data.address).length).toBe(2); + expect(getByTestId("sign-logo")).toBeInTheDocument(); + + expect(getByTestId("sign-logo").getAttribute("src")).not.toBe(undefined); + }); +}); + +describe("Sign JSON", () => { + const mockStore = configureStore(); + const dispatchMock = jest.fn(); + const storeMocked = { + ...mockStore(store.getState()), + dispatch: dispatchMock, + }; + + const pageId = "incoming-request"; + const activeStatus = true; + const blur = false; + const setBlur = jest.fn(); + const requestData = { + id: "abc123456", + label: "Cardano", + type: IncomingRequestType.PEER_CONNECT_SIGN, + signTransaction: signObjectFix, + peerConnection: { id: "id", name: "DApp" }, + }; + + const initiateAnimation = false; + const handleAccept = jest.fn(); + const handleCancel = jest.fn(); + const handleIgnore = jest.fn(); + + test("It renders content for BALLOT_TRANSACTION_REQUEST ", async () => { + const { getByText, getAllByText } = render( + + + + ); + + expect(getByText(requestData.peerConnection?.name)).toBeVisible(); + expect( + getByText(JSON.parse(signObjectFix.payload.payload).data.id) + ).toBeVisible(); }); test("Display fallback image when provider logo is empty: BALLOT_TRANSACTION_REQUEST", async () => { @@ -257,7 +335,7 @@ describe("Sign request", () => { handleAccept={handleAccept} handleCancel={handleCancel} handleIgnore={handleIgnore} - incomingRequestType={IncomingRequestType.SIGN_TRANSACTION_REQUEST} + incomingRequestType={IncomingRequestType.PEER_CONNECT_SIGN} /> ); diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.tsx index d907bb576..e4db9746d 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.tsx @@ -2,7 +2,7 @@ import { IncomingRequestType } from "../../../../../../store/reducers/stateCache import { RequestProps } from "../IncomingRequest.types"; import { CredentialRequest } from "./CredentialRequest"; import { MultiSigRequest } from "./MultiSigRequest"; -import { SignTransactionRequest } from "./SignTransactionRequest"; +import { SignRequest } from "./SignRequest"; const RequestComponent = ({ pageId, @@ -43,9 +43,9 @@ const RequestComponent = ({ handleIgnore={handleIgnore} /> ); - case IncomingRequestType.SIGN_TRANSACTION_REQUEST: + case IncomingRequestType.PEER_CONNECT_SIGN: return ( - { + const signRequest = requestData.signTransaction; + const [isSigningObject, setIsSigningObject] = useState(false); + const logo = requestData.logo ? requestData.logo : CardanoLogo; + + const signDetails = useMemo(() => { + if (!signRequest) return {}; + + let signContent; + try { + signContent = JSON.parse(signRequest.payload.payload); + setIsSigningObject(true); + } catch (error) { + signContent = signRequest.payload.payload; + } + return signContent; + }, [requestData.signTransaction]); + + const handleSign = () => { + handleAccept(); + }; + + return ( + {`${i18n.t("request.sign.title")}`}} + > +
+ {requestData.peerConnection?.name} +

{requestData.peerConnection?.name}

+

{requestData.peerConnection?.url}

+
+
+ + + {signRequest?.payload.identifier} + + + + {isSigningObject ? ( + + ) : ( + {signDetails.toString()} + )} + +
+ +
+ ); +}; + +export { SignRequest }; diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/SignTransactionRequest.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/SignTransactionRequest.tsx deleted file mode 100644 index bed40cd99..000000000 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/SignTransactionRequest.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { IonText } from "@ionic/react"; -import { useMemo } from "react"; -import { i18n } from "../../../../../../i18n"; -import { - CardDetailsAttributes, - CardDetailsBlock, -} from "../../../../../components/CardDetails"; -import { PageFooter } from "../../../../../components/PageFooter"; -import { PageHeader } from "../../../../../components/PageHeader"; -import { ScrollablePageLayout } from "../../../../../components/layout/ScrollablePageLayout"; -import CardanoLogo from "../../../../../assets/images/CardanoLogo.jpg"; -import { RequestProps } from "../IncomingRequest.types"; -import "./SignTransactionRequest.scss"; - -const SignTransactionRequest = ({ - pageId, - activeStatus, - requestData, - handleAccept, - handleCancel, -}: RequestProps) => { - const signTransaction = requestData.signTransaction; - const ballotLogo = requestData.logo ? requestData.logo : CardanoLogo; - - const signDetails = useMemo(() => { - if (!signTransaction) return {}; - - return { - [`${i18n.t("request.signtransaction.transaction.action")}`]: - signTransaction.action, - [`${i18n.t("request.signtransaction.transaction.actionText")}`]: - signTransaction.actionText, - [`${i18n.t("request.signtransaction.transaction.id")}`]: - signTransaction.data.id, - [`${i18n.t("request.signtransaction.transaction.address")}`]: - signTransaction.data.address, - [`${i18n.t("request.signtransaction.transaction.event")}`]: - signTransaction.data.event, - [`${i18n.t("request.signtransaction.transaction.category")}`]: - signTransaction.data.category, - [`${i18n.t("request.signtransaction.transaction.proposal")}`]: - signTransaction.data.proposal, - [`${i18n.t("request.signtransaction.transaction.network")}`]: - signTransaction.data.network, - [`${i18n.t("request.signtransaction.transaction.votedat")}`]: - signTransaction.data.votedAt, - [`${i18n.t("request.signtransaction.transaction.votingpower")}`]: - signTransaction.data.votingPower, - [`${i18n.t("request.signtransaction.transaction.slot")}`]: - signTransaction.slot, - [`${i18n.t("request.signtransaction.transaction.uri")}`]: - signTransaction.uri, - }; - }, [signTransaction]); - - const handleSign = () => { - handleAccept(); - }; - - return ( - - } - > -
- {requestData.label} -

{requestData.label}

-

{signTransaction?.eventName}

-

{signTransaction?.ownerUrl}

-
-
- - {signTransaction?.data.address} - - - - -
- -
- ); -}; - -export { SignTransactionRequest }; diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/SignTransactionRequest.types.ts b/src/ui/pages/SidePage/components/IncomingRequest/components/SignTransactionRequest.types.ts deleted file mode 100644 index ff74c09a6..000000000 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/SignTransactionRequest.types.ts +++ /dev/null @@ -1,23 +0,0 @@ -// TODO: Dummy type for ballot transaction. Should be remove when implement logic from core -interface SignTransactionData { - id: string; - address: string; - event: string; - category: string; - proposal: string; - network: string; - votedAt: string; - votingPower: string; -} - -interface SignTransaction { - action: string; - actionText: string; - data: SignTransactionData; - slot: string; - uri: string; - ownerUrl: string; - eventName: string; -} - -export type { SignTransaction, SignTransactionData }; diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.test.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.test.tsx index ab849c436..72f07fc0d 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.test.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.test.tsx @@ -10,13 +10,31 @@ import { WalletConnectStageOne } from "./WalletConnectStageOne"; import { WalletConnectStageTwo } from "./WalletConnectStageTwo"; import { identifierFix } from "../../../../__fixtures__/identifierFix"; import { TabsRoutePath } from "../../../../../routes/paths"; -import { walletConnectionsFix } from "../../../../__fixtures__/walletConnectionsFix"; import { setToastMsg } from "../../../../../store/reducers/stateCache"; import { ToastMsgType } from "../../../../globals/types"; import { WalletConnect } from "./WalletConnect"; +import { setWalletConnectionsCache } from "../../../../../store/reducers/walletConnectionsCache"; setupIonicReact(); mockIonicReact(); +jest.mock("../../../../../core/cardano/walletConnect/peerConnection", () => ({ + PeerConnection: { + peerConnection: { + start: jest.fn(), + connectWithDApp: jest.fn(), + }, + }, +})); +jest.mock("../../../../../core/agent/agent", () => ({ + Agent: { + agent: { + peerConnectionMetadataStorage: { + getPeerConnectionMetadata: jest.fn(), + getAllPeerConnectionMetadata: jest.fn(), + }, + }, + }, +})); jest.mock("@ionic/react", () => ({ ...jest.requireActual("@ionic/react"), IonModal: ({ children, isOpen }: any) => ( @@ -167,7 +185,7 @@ describe("Wallet Connect Stage Two", () => { @@ -196,7 +214,7 @@ describe("Wallet Connect Stage Two", () => { @@ -222,9 +240,7 @@ describe("Wallet Connect Stage Two", () => { }); await waitFor(() => { - expect(dispatchMock).toBeCalledWith( - setToastMsg(ToastMsgType.CONNECT_WALLET_SUCCESS) - ); + expect(dispatchMock).toBeCalled(); }); }); }); @@ -244,7 +260,7 @@ describe("Wallet Connect Request", () => { }, walletConnectionsCache: { walletConnections: [], - pendingConnection: walletConnectionsFix[0], + pendingDAppMeerKat: "pending-meerkat", }, identifiersCache: { identifiers: [...identifierFix], @@ -304,9 +320,7 @@ describe("Wallet Connect Request", () => { }); await waitFor(() => { - expect(dispatchMock).toBeCalledWith( - setToastMsg(ToastMsgType.CONNECT_WALLET_SUCCESS) - ); + expect(dispatchMock).toBeCalled(); }); }); diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx index bcfed0927..af18746bb 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; import { useAppDispatch, useAppSelector } from "../../../../../store/hooks"; import { - getPendingConnection, - setPendingConnections, + getPendingDAppMeerkat, + setPendingDAppMeerKat, } from "../../../../../store/reducers/walletConnectionsCache"; import { WalletConnectStageOne } from "./WalletConnectStageOne"; import { WalletConnectStageTwo } from "./WalletConnectStageTwo"; @@ -10,12 +10,12 @@ import { SidePageContentProps } from "../../SidePage.types"; const WalletConnect = ({ setOpenPage }: SidePageContentProps) => { const dispatch = useAppDispatch(); - const pendingConnection = useAppSelector(getPendingConnection); + const pendingDAppMeerkat = useAppSelector(getPendingDAppMeerkat); const [requestStage, setRequestStage] = useState(0); useEffect(() => { - setTimeout(() => setOpenPage(!!pendingConnection), 10); - }, [pendingConnection]); + setTimeout(() => setOpenPage(!!pendingDAppMeerkat), 10); + }, [pendingDAppMeerkat]); const changeToStageTwo = () => { setRequestStage(1); @@ -25,25 +25,26 @@ const WalletConnect = ({ setOpenPage }: SidePageContentProps) => { setOpenPage(false); setTimeout(() => { - dispatch(setPendingConnections(null)); + dispatch(setPendingDAppMeerKat(null)); }, 500); }; - if (!pendingConnection) return null; + if (!pendingDAppMeerkat) return null; - if (requestStage === 0) + if (requestStage === 0) { return ( ); + } return ( setRequestStage(0)} /> diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.types.ts b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.types.ts index 92de908a5..c5e7881d6 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.types.ts +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.types.ts @@ -1,5 +1,3 @@ -import { ConnectionData } from "../../../../../store/reducers/walletConnectionsCache"; - interface WalletConnectStageOneProps { isOpen: boolean; onClose: () => void; @@ -9,7 +7,7 @@ interface WalletConnectStageOneProps { interface WalletConnectStageTwoProps { isOpen: boolean; - data: ConnectionData; + pendingDAppMeerkat: string; onClose: () => void; onBackClick: () => void; className?: string; diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx index d435d3b16..1fbf732d1 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx @@ -14,15 +14,23 @@ import { ToastMsgType } from "../../../../globals/types"; import { combineClassNames } from "../../../../utils/style"; import "./WalletConnect.scss"; import { WalletConnectStageTwoProps } from "./WalletConnect.types"; +import { PeerConnection } from "../../../../../core/cardano/walletConnect/peerConnection"; +import { Agent } from "../../../../../core/agent/agent"; +import { + getWalletConnectionsCache, + setWalletConnectionsCache, +} from "../../../../../store/reducers/walletConnectionsCache"; const WalletConnectStageTwo = ({ isOpen, + pendingDAppMeerkat, className, onBackClick, onClose, }: WalletConnectStageTwoProps) => { const dispatch = useDispatch(); const indentifierCache = useAppSelector(getIdentifiersCache); + const existingConnections = useAppSelector(getWalletConnectionsCache); const [selectedIdentifier, setSelectedIdentifier] = useState(null); @@ -47,8 +55,17 @@ const WalletConnectStageTwo = ({ const handleConnectWallet = async () => { try { - // TODO: implement connect wallet logic - dispatch(setToastMsg(ToastMsgType.CONNECT_WALLET_SUCCESS)); + if (selectedIdentifier && pendingDAppMeerkat) { + await PeerConnection.peerConnection.start(selectedIdentifier.id); + await PeerConnection.peerConnection.connectWithDApp(pendingDAppMeerkat); + // Refresh the connections list + dispatch( + setWalletConnectionsCache([ + { id: pendingDAppMeerkat, selectedAid: selectedIdentifier.id }, + ...existingConnections, + ]) + ); + } onClose(); } catch (e) { dispatch(setToastMsg(ToastMsgType.UNABLE_CONNECT_WALLET)); From 9b8d9149a89e30833b5c1268409f4dc91e1c4e24 Mon Sep 17 00:00:00 2001 From: Patrick Nguyen Date: Tue, 4 Jun 2024 21:22:32 +0700 Subject: [PATCH 04/28] feat: peer connect error handling (#501) * feat: replace mock peerconnection with the real functions * refactor: update sign so it can sign arbitrary data * refactor: update sign so it can sign arbitrary data * update: small UI updates * update: small UI updates * refactor: rename css classes and event type * update: change css for string data signing input * refactor: remove label in incoming signing request since it is duplicated * feat: peer connect redux updates * feat: peer connect error handling * feat: add unit tests for peer connection handlers * feat: add unit tests for peer connection broken handler * refactor: setConnectedWallet with the dApp address * refactor: empty payload for broken peer connection event * update: navigate to create new identifier * refactor: update the peer connections list with a prototype connection * feat: check the id of the connected DApp before handle the sign request * refactor: change connectwallet.connectwalletmodal.connectionbrokenalertto connectwallet.connectionbrokenalert --- .../agent/services/identifierService.test.ts | 38 +++++++++++++++++++ src/core/agent/services/identifierService.ts | 7 ++++ .../walletConnect/identityWalletConnect.ts | 5 +++ .../cardano/walletConnect/peerConnection.ts | 27 ++++++++++++- .../walletConnect/peerConnection.types.ts | 7 ++++ src/locales/en/en.json | 4 ++ .../components/AppWrapper/AppWrapper.test.tsx | 18 +++++++++ src/ui/components/AppWrapper/AppWrapper.tsx | 37 +++++++++++++++++- .../ConnectWallet/ConnectWallet.tsx | 4 ++ .../IncomingRequest/IncomingRequest.tsx | 22 ++++++++--- .../WalletConnect/WalletConnectStageTwo.tsx | 5 +-- 11 files changed, 164 insertions(+), 10 deletions(-) diff --git a/src/core/agent/services/identifierService.test.ts b/src/core/agent/services/identifierService.test.ts index a3a366c92..01bf40a5e 100644 --- a/src/core/agent/services/identifierService.test.ts +++ b/src/core/agent/services/identifierService.test.ts @@ -1,3 +1,4 @@ +import { PeerConnection } from "../../cardano/walletConnect/peerConnection"; import { Agent } from "../agent"; import { IdentifierMetadataRecord } from "../records/identifierMetadataRecord"; import { EventService } from "./eventService"; @@ -106,6 +107,16 @@ jest.mock("../../../core/agent/agent", () => ({ }, })); +jest.mock("../../cardano/walletConnect/peerConnection", () => ({ + PeerConnection: { + peerConnection: { + getConnectedDAppAddress: jest.fn(), + getConnectingAid: jest.fn(), + disconnectDApp: jest.fn(), + }, + }, +})); + const now = new Date(); const nowISO = now.toISOString(); @@ -268,6 +279,33 @@ describe("Single sig service of agent", () => { ); }); + test("can delete an archived identifier and disconnect DApp", async () => { + identifierStorage.getIdentifierMetadata = jest + .fn() + .mockResolvedValue(archivedMetadataRecord); + identifierStorage.updateIdentifierMetadata = jest.fn(); + PeerConnection.peerConnection.getConnectedDAppAddress = jest + .fn() + .mockReturnValue("dApp-address"); + PeerConnection.peerConnection.getConnectingAid = jest + .fn() + .mockReturnValue(archivedMetadataRecord.id); + await identifierService.deleteIdentifier(archivedMetadataRecord.id); + expect(identifierStorage.getIdentifierMetadata).toBeCalledWith( + archivedMetadataRecord.id + ); + expect(identifierStorage.updateIdentifierMetadata).toBeCalledWith( + archivedMetadataRecord.id, + { + isDeleted: true, + } + ); + expect(PeerConnection.peerConnection.disconnectDApp).toBeCalledWith( + "dApp-address", + true + ); + }); + test("cannot delete a non-archived credential", async () => { identifierStorage.getIdentifierMetadata = jest .fn() diff --git a/src/core/agent/services/identifierService.ts b/src/core/agent/services/identifierService.ts index f97da66f8..c415a2e6c 100644 --- a/src/core/agent/services/identifierService.ts +++ b/src/core/agent/services/identifierService.ts @@ -15,6 +15,7 @@ import { AgentServicesProps, IdentifierResult } from "../agent.types"; import { IdentifierStorage } from "../records"; import { ConfigurationService } from "../../configuration"; import { BackingMode } from "../../configuration/configurationService.types"; +import { PeerConnection } from "../../cardano/walletConnect/peerConnection"; const identifierTypeThemes = [0, 1]; @@ -152,10 +153,16 @@ class IdentifierService extends AgentService { const metadata = await this.identifierStorage.getIdentifierMetadata( identifier ); + const connectedDApp = + PeerConnection.peerConnection.getConnectedDAppAddress(); + const peerConnectingAid = PeerConnection.peerConnection.getConnectingAid(); this.validArchivedIdentifier(metadata); await this.identifierStorage.updateIdentifierMetadata(identifier, { isDeleted: true, }); + if (connectedDApp !== "" && metadata.id === peerConnectingAid) { + PeerConnection.peerConnection.disconnectDApp(connectedDApp, true); + } } async restoreIdentifier(identifier: string): Promise { diff --git a/src/core/cardano/walletConnect/identityWalletConnect.ts b/src/core/cardano/walletConnect/identityWalletConnect.ts index c8c059a01..9263fa1cd 100644 --- a/src/core/cardano/walletConnect/identityWalletConnect.ts +++ b/src/core/cardano/walletConnect/identityWalletConnect.ts @@ -27,6 +27,7 @@ class IdentityWalletConnect extends CardanoPeerConnect { identifier: string, payload: string ) => Promise; + getConnectingAid: () => string; signerCache: Map; @@ -98,6 +99,10 @@ class IdentityWalletConnect extends CardanoPeerConnect { return { error: TxSignError.UserDeclined }; } }; + + this.getConnectingAid = () => { + return this.selectedAid; + }; } protected getNetworkId(): Promise { diff --git a/src/core/cardano/walletConnect/peerConnection.ts b/src/core/cardano/walletConnect/peerConnection.ts index ac662cfa8..421281dfe 100644 --- a/src/core/cardano/walletConnect/peerConnection.ts +++ b/src/core/cardano/walletConnect/peerConnection.ts @@ -10,6 +10,7 @@ import { ExperimentalAPIFunctions, PeerConnectSigningEvent, PeerConnectedEvent, + PeerConnectionBrokenEvent, PeerConnectionEventTypes, PeerDisconnectedEvent, } from "./peerConnection.types"; @@ -72,6 +73,17 @@ class PeerConnection { ); } + onPeerConnectionBrokenStateChanged( + callback: (event: PeerConnectionBrokenEvent) => void + ) { + this.eventService.on( + PeerConnectionEventTypes.PeerConnectionBroken, + async (event: PeerConnectionBrokenEvent) => { + callback(event); + } + ); + } + static get peerConnection() { if (!this.instance) { this.instance = new PeerConnection(); @@ -153,6 +165,7 @@ class PeerConnection { new ExperimentalContainer({ getIdentifierOobi: this.identityWalletConnect.getIdentifierOobi, sign: this.identityWalletConnect.sign, + getConnectingAid: this.identityWalletConnect.getConnectingAid, }) ); } @@ -178,6 +191,7 @@ class PeerConnection { await Agent.agent.peerConnectionMetadataStorage.createPeerConnectionMetadataRecord( { id: dAppIdentifier, + selectedAid: this.identityWalletConnect.getConnectingAid(), iconB64: ICON_BASE64, } ); @@ -187,17 +201,28 @@ class PeerConnection { SecureStorage.set(KeyStoreKeys.MEERKAT_SEED, seed); } - disconnectDApp(dAppIdentifier: string) { + disconnectDApp(dAppIdentifier: string, isBroken?: boolean) { if (this.identityWalletConnect === undefined) { throw new Error(PeerConnection.PEER_CONNECTION_START_PENDING); } this.identityWalletConnect.disconnect(dAppIdentifier); + + if (isBroken) { + this.eventService.emit({ + type: PeerConnectionEventTypes.PeerConnectionBroken, + payload: {}, + }); + } } getConnectedDAppAddress() { return this.connectedDAppAdress; } + + getConnectingAid() { + return this.identityWalletConnect?.getConnectingAid(); + } } export { PeerConnection }; diff --git a/src/core/cardano/walletConnect/peerConnection.types.ts b/src/core/cardano/walletConnect/peerConnection.types.ts index 3953a28a3..f92b47ef1 100644 --- a/src/core/cardano/walletConnect/peerConnection.types.ts +++ b/src/core/cardano/walletConnect/peerConnection.types.ts @@ -9,12 +9,14 @@ interface ExperimentalAPIFunctions { identifier: string, payload: string ) => Promise; + getConnectingAid: () => string; } enum PeerConnectionEventTypes { PeerConnectSign = "PeerConnectSign", PeerConnected = "PeerConnected", PeerDisconnected = "PeerDisconnected", + PeerConnectionBroken = "PeerConnectionBroken", } interface PeerConnectSigningEvent extends BaseEventEmitter { @@ -41,6 +43,10 @@ interface PeerDisconnectedEvent extends BaseEventEmitter { }; } +interface PeerConnectionBrokenEvent extends BaseEventEmitter { + type: typeof PeerConnectionEventTypes.PeerConnectionBroken; +} + interface PeerConnectionError { code: number; info: string; @@ -61,5 +67,6 @@ export type { PeerConnectSigningEvent, PeerConnectedEvent, PeerDisconnectedEvent, + PeerConnectionBrokenEvent, PeerConnectionError, }; diff --git a/src/locales/en/en.json b/src/locales/en/en.json index 9390a3ee3..92f459ec3 100644 --- a/src/locales/en/en.json +++ b/src/locales/en/en.json @@ -877,6 +877,10 @@ "cancel": "Cancel" } }, + "connectionbrokenalert": { + "message": "Your connection has been disconnected as you have deleted the chosen identifier paired with this connection. Please choose a new identifier to re-establish this connection.", + "confirm": "Ok" + }, "inputpidmodal": { "header": "Paste Meerkat ID", "cancel": "Cancel", diff --git a/src/ui/components/AppWrapper/AppWrapper.test.tsx b/src/ui/components/AppWrapper/AppWrapper.test.tsx index 10f2a3a80..d95c2ce95 100644 --- a/src/ui/components/AppWrapper/AppWrapper.test.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.test.tsx @@ -7,6 +7,7 @@ import { keriaNotificationsChangeHandler, peerConnectRequestSignChangeHandler, peerConnectedChangeHandler, + peerConnectionBrokenChangeHandler, peerDisconnectedChangeHandler, } from "./AppWrapper"; import { store } from "../../../store"; @@ -37,6 +38,7 @@ import { import { PeerConnectSigningEvent, PeerConnectedEvent, + PeerConnectionBrokenEvent, PeerConnectionEventTypes, PeerDisconnectedEvent, } from "../../../core/cardano/walletConnect/peerConnection.types"; @@ -168,6 +170,11 @@ const peerSignRequestEventMock = { }, } as PeerConnectSigningEvent; +const peerConnectionBrokenEventMock = { + type: PeerConnectionEventTypes.PeerConnectionBroken, + payload: {}, +} as PeerConnectionBrokenEvent; + const peerConnectionMock: ConnectionData = { id: "dApp-address", name: "dApp-name", @@ -346,5 +353,16 @@ describe("AppWrapper handler", () => { }) ); }); + + test("handle peer connection broken event", async () => { + await peerConnectionBrokenChangeHandler( + peerConnectionBrokenEventMock, + dispatch + ); + expect(dispatch).toBeCalledWith(setConnectedWallet(null)); + expect(dispatch).toBeCalledWith( + setToastMsg(ToastMsgType.DISCONNECT_WALLET_SUCCESS) + ); + }); }); }); diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index 8bf7debf5..d5d8073ee 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -49,6 +49,7 @@ import { PeerConnection } from "../../../core/cardano/walletConnect/peerConnecti import { PeerConnectSigningEvent, PeerConnectedEvent, + PeerConnectionBrokenEvent, PeerDisconnectedEvent, } from "../../../core/cardano/walletConnect/peerConnection.types"; import { MultiSigService } from "../../../core/agent/services/multiSigService"; @@ -56,6 +57,7 @@ import { setViewTypeCache } from "../../../store/reducers/identifierViewTypeCach import { CardListViewType } from "../SwitchCardView"; import { setEnableBiometryCache } from "../../../store/reducers/biometryCache"; import { i18n } from "../../../i18n"; +import { Alert } from "../Alert"; const connectionStateChangedHandler = async ( event: ConnectionStateChangedEvent, @@ -183,6 +185,14 @@ const peerDisconnectedChangeHandler = async ( } }; +const peerConnectionBrokenChangeHandler = async ( + event: PeerConnectionBrokenEvent, + dispatch: ReturnType +) => { + dispatch(setConnectedWallet(null)); + dispatch(setToastMsg(ToastMsgType.DISCONNECT_WALLET_SUCCESS)); +}; + const AppWrapper = (props: { children: ReactNode }) => { const dispatch = useAppDispatch(); const authentication = useAppSelector(getAuthentication); @@ -190,6 +200,7 @@ const AppWrapper = (props: { children: ReactNode }) => { const connectedWallet = useAppSelector(getConnectedWallet); const [isOnline, setIsOnline] = useState(false); const [isMessagesHandled, setIsMessagesHandled] = useState(false); + const [isAlertPeerBrokenOpen, setIsAlertPeerBrokenOpen] = useState(false); useActivityTimer(); useEffect(() => { @@ -380,10 +391,33 @@ const AppWrapper = (props: { children: ReactNode }) => { } } ); + PeerConnection.peerConnection.onPeerConnectionBrokenStateChanged( + async (event) => { + setIsAlertPeerBrokenOpen(true); + return peerConnectionBrokenChangeHandler(event, dispatch); + } + ); dispatch(setInitialized(true)); }; - return <>{props.children}; + return ( + <> + {props.children} + dispatch(setCurrentOperation(OperationType.IDLE))} + actionDismiss={() => dispatch(setCurrentOperation(OperationType.IDLE))} + /> + + ); }; export { @@ -394,4 +428,5 @@ export { peerConnectedChangeHandler, peerDisconnectedChangeHandler, peerConnectRequestSignChangeHandler, + peerConnectionBrokenChangeHandler, }; diff --git a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx index 4218dd103..805abd255 100644 --- a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx +++ b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx @@ -136,6 +136,10 @@ const ConnectWallet = forwardRef( }; const handleConnectWallet = () => { + if (identifierCache.length === 0) { + setOpenIdentifierMissingAlert(true); + return; + } if (!actionInfo.current.data) return; const isConnectedItem = actionInfo.current.data.id === connectedWallet; if (isConnectedItem) { diff --git a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx index 0aca4bf4b..a9eee7588 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx @@ -12,17 +12,22 @@ import { IncomingRequestProps, IncomingRequestType, } from "../../../../../store/reducers/stateCache/stateCache.types"; +import { getConnectedWallet } from "../../../../../store/reducers/walletConnectionsCache"; const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { const pageId = "incoming-request"; const dispatch = useAppDispatch(); const queueIncomingRequest = useAppSelector(getQueueIncomingRequest); + const connectedWallet = useAppSelector(getConnectedWallet); const incomingRequest = useMemo(() => { - return !queueIncomingRequest.isProcessing - ? { id: "" } - : queueIncomingRequest.queues.length > 0 - ? queueIncomingRequest.queues[0] - : { id: "" }; + if ( + !queueIncomingRequest.isProcessing || + !queueIncomingRequest.queues.length + ) { + return { id: "" }; + } else { + return queueIncomingRequest.queues[0]; + } }, [queueIncomingRequest]); const [initiateAnimation, setInitiateAnimation] = useState(false); const [requestData, setRequestData] = useState(); @@ -30,6 +35,13 @@ const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { const [blur, setBlur] = useState(false); useEffect(() => { + if ( + incomingRequest.type === IncomingRequestType.PEER_CONNECT_SIGN && + (!connectedWallet || + connectedWallet !== incomingRequest.peerConnection?.id) + ) { + handleReset(); + } if (incomingRequest.id.length > 0) { setRequestData(incomingRequest); setOpenPage(true); diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx index 1fbf732d1..ef8a109fe 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx @@ -29,13 +29,12 @@ const WalletConnectStageTwo = ({ onClose, }: WalletConnectStageTwoProps) => { const dispatch = useDispatch(); - const indentifierCache = useAppSelector(getIdentifiersCache); + const identifierCache = useAppSelector(getIdentifiersCache); const existingConnections = useAppSelector(getWalletConnectionsCache); const [selectedIdentifier, setSelectedIdentifier] = useState(null); - - const displayIdentifiers = indentifierCache.map( + const displayIdentifiers = identifierCache.map( (identifier, index): CardItem => ({ id: index, title: identifier.displayName, From 5cc4da7c392b4474b025daacde0649900982b583 Mon Sep 17 00:00:00 2001 From: Bao Hoang <142210850+bao-sotatek@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:19:39 +0700 Subject: [PATCH 05/28] feat: SSI agent onboarding (KERIA config) (#493) * feat: add func load db * feat: add func initDatabase, save keri url and boot url, handle on start up * feat: change agent function, add bootAndConnect func * chore: fix unittest * chore: update text KERIA_BOOT_URL * chore: update start func * chore: resolve check already booted and some var name * feat: set SignifyClient * fix: multiSigs undefined * fix: init server * feat(ui): SSI Agent Page (#500) * feat(ui): update redux store * feat(ui): create ssi agent page * feat(ui): fix review comment * feat(ui): update ssi loading indicator styles --------- Co-authored-by: Vu Van Duc * chore: url auto populate the UI onboarding --------- Co-authored-by: Sotatek-DukeVu <162310763+Sotatek-DukeVu@users.noreply.github.com> Co-authored-by: Vu Van Duc --- src/core/agent/agent.ts | 147 +++++--- src/core/agent/agent.types.ts | 8 + src/core/agent/services/agentService.ts | 8 +- src/core/agent/services/connectionService.ts | 16 +- src/core/agent/services/credentialService.ts | 8 +- src/core/agent/services/delegationService.ts | 12 +- src/core/agent/services/identifierService.ts | 18 +- .../services/ipexCommunicationService.ts | 36 +- src/core/agent/services/multiSigService.ts | 60 ++-- .../services/signifyNotificationService.ts | 10 +- src/core/agent/services/utils.ts | 2 +- .../configuration/configurationService.ts | 26 +- .../configurationService.types.ts | 6 +- src/locales/en/en.json | 32 ++ src/routes/backRoute/backRoute.test.ts | 10 + src/routes/backRoute/backRoute.ts | 3 + src/routes/index.tsx | 7 + src/routes/nextRoute/nextRoute.test.ts | 21 +- src/routes/nextRoute/nextRoute.ts | 57 +++- src/routes/paths.ts | 1 + src/store/index.ts | 7 +- src/store/reducers/ssiAgent/index.ts | 1 + src/store/reducers/ssiAgent/ssiAgent.test.ts | 73 ++++ src/store/reducers/ssiAgent/ssiAgent.ts | 43 +++ src/store/reducers/ssiAgent/ssiAgent.types.ts | 6 + .../reducers/stateCache/stateCache.test.ts | 1 + src/store/reducers/stateCache/stateCache.ts | 1 + .../reducers/stateCache/stateCache.types.ts | 1 + src/ui/App.test.tsx | 1 + src/ui/App.tsx | 2 + .../components/AppWrapper/AppWrapper.test.tsx | 1 + src/ui/components/AppWrapper/AppWrapper.tsx | 51 +-- src/ui/components/CustomInput/CustomInput.tsx | 17 + .../CustomInput/CustomInput.types.ts | 4 +- src/ui/components/Scanner/Scanner.tsx | 24 ++ src/ui/globals/types.ts | 2 + .../pages/CreateSSIAgent/CreateSSIAgent.scss | 55 +++ .../CreateSSIAgent/CreateSSIAgent.test.tsx | 322 ++++++++++++++++++ .../pages/CreateSSIAgent/CreateSSIAgent.tsx | 290 ++++++++++++++++ .../CreateSSIAgent/CreateSSIAgent.types.ts | 9 + src/ui/pages/CreateSSIAgent/index.ts | 1 + src/ui/utils/urlChecker.test.ts | 31 ++ src/ui/utils/urlChecker.ts | 10 + 43 files changed, 1249 insertions(+), 192 deletions(-) create mode 100644 src/store/reducers/ssiAgent/index.ts create mode 100644 src/store/reducers/ssiAgent/ssiAgent.test.ts create mode 100644 src/store/reducers/ssiAgent/ssiAgent.ts create mode 100644 src/store/reducers/ssiAgent/ssiAgent.types.ts create mode 100644 src/ui/pages/CreateSSIAgent/CreateSSIAgent.scss create mode 100644 src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx create mode 100644 src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx create mode 100644 src/ui/pages/CreateSSIAgent/CreateSSIAgent.types.ts create mode 100644 src/ui/pages/CreateSSIAgent/index.ts create mode 100644 src/ui/utils/urlChecker.test.ts create mode 100644 src/ui/utils/urlChecker.ts diff --git a/src/core/agent/agent.ts b/src/core/agent/agent.ts index de923adc0..4e8aa04f0 100644 --- a/src/core/agent/agent.ts +++ b/src/core/agent/agent.ts @@ -18,6 +18,8 @@ import { BranAndMnemonic, KeriaStatusChangedEvent, KeriaStatusEventTypes, + AgentUrls, + MiscRecordId, } from "./agent.types"; import { EventService } from "./services/eventService"; import { @@ -50,9 +52,15 @@ const walletId = "idw"; class Agent { static readonly KERIA_CONNECTION_BROKEN = "The app is not connected to KERIA at the moment"; + static readonly KERIA_BOOT_FAILED = "Failed to boot signify client"; + static readonly KERIA_BOOTED_ALREADY_BUT_CANNOT_CONNECT = + "Signify client is already booted but cannot connect"; private static instance: Agent; - private agentServicesProps!: AgentServicesProps; + private agentServicesProps: AgentServicesProps = { + eventService: undefined as any, + signifyClient: undefined as any, + }; private storageSession!: SqliteSession | IonicSession; @@ -174,68 +182,112 @@ class Agent { ); } - async start(): Promise { + async start(keriaConnectUrl: string): Promise { if (!Agent.isOnline) { - await this.storageSession.open(walletId); - this.basicStorageService = new BasicStorage( - this.getStorageService(this.storageSession) - ); - this.identifierStorage = new IdentifierStorage( - this.getStorageService(this.storageSession) - ); - this.credentialStorage = new CredentialStorage( - this.getStorageService(this.storageSession) - ); - this.connectionStorage = new ConnectionStorage( - this.getStorageService(this.storageSession) - ); - this.connectionNoteStorage = new ConnectionNoteStorage( - this.getStorageService(this.storageSession) - ); - this.notificationStorage = new NotificationStorage( - this.getStorageService(this.storageSession) - ); + await signifyReady(); + const bran = await this.getBran(); + this.signifyClient = new SignifyClient(keriaConnectUrl, bran, Tier.low); + await this.signifyClient.connect(); + Agent.isOnline = true; + this.agentServicesProps.signifyClient = this.signifyClient; + this.agentServicesProps.eventService.emit({ + type: KeriaStatusEventTypes.KeriaStatusChanged, + payload: { + isOnline: Agent.isOnline, + }, + }); + } + } + async bootAndConnect(agentUrls: AgentUrls): Promise { + if (!Agent.isOnline) { await signifyReady(); const bran = await this.getBran(); - // @TODO - foconnor: Review of Tier level. this.signifyClient = new SignifyClient( - ConfigurationService.env.keri.keria.url, + agentUrls.url, bran, Tier.low, - ConfigurationService.env.keri.keria.bootUrl + agentUrls.bootUrl ); - - this.agentServicesProps = { - signifyClient: this.signifyClient, - eventService: new EventService(), - }; - - this.peerConnectionStorage = new PeerConnectionStorage( - this.getStorageService( - this.storageSession - ) - ); - + try { + const bootResponse = await this.signifyClient.boot(); + const bootResponseBody = await bootResponse.json(); + if ( + bootResponse.status !== 202 && + bootResponseBody?.title !== "agent already exists" + ) { + throw new Error(Agent.KERIA_BOOT_FAILED); + } + } catch (e) { + /* eslint-disable no-console */ + console.error(e); + throw new Error(Agent.KERIA_BOOT_FAILED); + } + try { + await this.signifyClient.connect(); + } catch (e) { + /* eslint-disable no-console */ + console.error(e); + throw new Error(Agent.KERIA_BOOTED_ALREADY_BUT_CANNOT_CONNECT); + } + await this.saveAgentUrls(agentUrls); + Agent.isOnline = true; + this.agentServicesProps.signifyClient = this.signifyClient; this.agentServicesProps.eventService.emit({ type: KeriaStatusEventTypes.KeriaStatusChanged, payload: { isOnline: Agent.isOnline, }, }); - - try { - await this.signifyClient.connect(); - Agent.isOnline = true; - } catch (err) { - await this.signifyClient.boot(); - await this.signifyClient.connect(); - Agent.isOnline = true; - } } } - async bootAndConnect(retryInterval = 1000) { + private async saveAgentUrls(agentUrls: AgentUrls): Promise { + await this.basicStorageService.save({ + id: MiscRecordId.KERIA_CONNECT_URL, + content: { + url: agentUrls.url, + }, + }); + await this.basicStorageService.save({ + id: MiscRecordId.KERIA_BOOT_URL, + content: { + url: agentUrls.bootUrl, + }, + }); + } + + async initDatabaseConnection(): Promise { + await this.storageSession.open(walletId); + this.basicStorageService = new BasicStorage( + this.getStorageService(this.storageSession) + ); + this.identifierStorage = new IdentifierStorage( + this.getStorageService(this.storageSession) + ); + this.credentialStorage = new CredentialStorage( + this.getStorageService(this.storageSession) + ); + this.connectionStorage = new ConnectionStorage( + this.getStorageService(this.storageSession) + ); + this.connectionNoteStorage = new ConnectionNoteStorage( + this.getStorageService(this.storageSession) + ); + this.notificationStorage = new NotificationStorage( + this.getStorageService(this.storageSession) + ); + this.peerConnectionStorage = new PeerConnectionStorage( + this.getStorageService(this.storageSession) + ); + + this.agentServicesProps = { + signifyClient: this.signifyClient, + eventService: new EventService(), + }; + } + + async connect(retryInterval = 1000) { try { if (Agent.isOnline) { Agent.isOnline = false; @@ -246,7 +298,6 @@ class Agent { }, }); } - await this.signifyClient.boot(); await this.signifyClient.connect(); Agent.isOnline = true; this.agentServicesProps.eventService.emit({ @@ -257,7 +308,7 @@ class Agent { }); } catch (error) { await new Promise((resolve) => setTimeout(resolve, retryInterval)); - await this.bootAndConnect(retryInterval); + await this.connect(retryInterval); } } diff --git a/src/core/agent/agent.types.ts b/src/core/agent/agent.types.ts index 47775c550..986f73d39 100644 --- a/src/core/agent/agent.types.ts +++ b/src/core/agent/agent.types.ts @@ -29,6 +29,8 @@ enum MiscRecordId { APP_BIOMETRY = "app-biometry", KERIA_NOTIFICATION_MARKER = "keria-notification-marker", APP_IDENTIFIER_VIEW_TYPE = "app-identifier-view-type", + KERIA_CONNECT_URL = "keria-connect-url", + KERIA_BOOT_URL = "keria-boot-url", } interface ConnectionShortDetails { @@ -136,6 +138,11 @@ interface IdentifierResult { salty: any; } +interface AgentUrls { + url: string; + bootUrl: string; +} + enum NotificationRoute { ExnIpexGrant = "/exn/ipex/grant", MultiSigIcp = "/multisig/icp", @@ -175,5 +182,6 @@ export type { CreateIdentifierResult, IdentifierResult, KeriaStatusChangedEvent, + AgentUrls, BranAndMnemonic, }; diff --git a/src/core/agent/services/agentService.ts b/src/core/agent/services/agentService.ts index 7062a4dda..279aab9ed 100644 --- a/src/core/agent/services/agentService.ts +++ b/src/core/agent/services/agentService.ts @@ -1,14 +1,10 @@ -import { SignifyClient } from "signify-ts"; -import { EventService } from "./eventService"; import { AgentServicesProps } from "../agent.types"; abstract class AgentService { - protected readonly signifyClient: SignifyClient; - protected readonly eventService: EventService; + protected props: AgentServicesProps; constructor(agentServicesProps: AgentServicesProps) { - this.signifyClient = agentServicesProps.signifyClient; - this.eventService = agentServicesProps.eventService; + this.props = agentServicesProps; } } diff --git a/src/core/agent/services/connectionService.ts b/src/core/agent/services/connectionService.ts index e8a5e47c2..722281a04 100644 --- a/src/core/agent/services/connectionService.ts +++ b/src/core/agent/services/connectionService.ts @@ -54,7 +54,7 @@ class ConnectionService extends AgentService { onConnectionStateChanged( callback: (event: ConnectionStateChangedEvent) => void ) { - this.eventService.on( + this.props.eventService.on( ConnectionEventTypes.ConnectionStateChanged, async (event: ConnectionStateChangedEvent) => { callback(event); @@ -68,7 +68,7 @@ class ConnectionService extends AgentService { // @TODO - foconnor: We shouldn't emit this if it's a multiSigInvite, but the routing will break if we don't. // To fix once we handle errors for the scanner in general. - this.eventService.emit({ + this.props.eventService.emit({ type: ConnectionEventTypes.ConnectionStateChanged, payload: { connectionId: undefined, @@ -99,7 +99,7 @@ class ConnectionService extends AgentService { await this.createConnectionMetadata(connectionId, connectionMetadata); if (!multiSigInvite) { - this.eventService.emit({ + this.props.eventService.emit({ type: ConnectionEventTypes.ConnectionStateChanged, payload: { connectionId: operation.response.i, @@ -151,7 +151,7 @@ class ConnectionService extends AgentService { @OnlineOnly async getConnectionById(id: string): Promise { - const connection = await this.signifyClient.contacts().get(id); + const connection = await this.props.signifyClient.contacts().get(id); return { label: connection?.alias, id: connection.id, @@ -218,7 +218,7 @@ class ConnectionService extends AgentService { alias?: string, groupId?: string ): Promise { - const result = await this.signifyClient + const result = await this.props.signifyClient .oobis() .get(signifyName, ConnectionService.DEFAULT_ROLE); const oobi = new URL(result.oobis[0]); @@ -272,7 +272,7 @@ class ConnectionService extends AgentService { // @TODO - foconnor: Contacts that are smid/rmids for multisigs will be synced too. @OnlineOnly async syncKeriaContacts() { - const signifyContacts = await this.signifyClient.contacts().list(); + const signifyContacts = await this.props.signifyClient.contacts().list(); const storageContacts = await this.connectionStorage.getAll(); const unSyncedData = signifyContacts.filter( (contact: KeriaContact) => @@ -296,8 +296,8 @@ class ConnectionService extends AgentService { } const alias = new URL(url).searchParams.get("name") ?? uuidv4(); const operation = await waitAndGetDoneOp( - this.signifyClient, - await this.signifyClient.oobis().resolve(url, alias) + this.props.signifyClient, + await this.props.signifyClient.oobis().resolve(url, alias) ); if (!operation.done) { throw new Error(ConnectionService.FAILED_TO_RESOLVE_OOBI); diff --git a/src/core/agent/services/credentialService.ts b/src/core/agent/services/credentialService.ts index e3252e3b5..ed32a58f4 100644 --- a/src/core/agent/services/credentialService.ts +++ b/src/core/agent/services/credentialService.ts @@ -36,7 +36,7 @@ class CredentialService extends AgentService { } onAcdcStateChanged(callback: (event: AcdcStateChangedEvent) => void) { - this.eventService.on( + this.props.eventService.on( AcdcEventTypes.AcdcStateChanged, async (event: AcdcStateChangedEvent) => { callback(event); @@ -81,7 +81,7 @@ class CredentialService extends AgentService { const metadata = await this.getMetadataById(id); let acdc; - const results = await this.signifyClient.credentials().list({ + const results = await this.props.signifyClient.credentials().list({ filter: { "-d": { $eq: metadata.id.replace("metadata:", "") }, }, @@ -192,7 +192,9 @@ class CredentialService extends AgentService { @OnlineOnly async syncACDCs() { - const signifyCredentials = await this.signifyClient.credentials().list(); + const signifyCredentials = await this.props.signifyClient + .credentials() + .list(); const storedCredentials = await this.credentialStorage.getAllCredentialMetadata(); const unSyncedData = signifyCredentials.filter( diff --git a/src/core/agent/services/delegationService.ts b/src/core/agent/services/delegationService.ts index d5a66d276..426764d69 100644 --- a/src/core/agent/services/delegationService.ts +++ b/src/core/agent/services/delegationService.ts @@ -27,7 +27,7 @@ class DelegationService extends AgentService { delegatorPrefix: string ): Promise { const signifyName = uuidv4(); - const operation = await this.signifyClient + const operation = await this.props.signifyClient .identifiers() .create(signifyName, { delpre: delegatorPrefix }); const identifier = operation.serder.ked.i; @@ -50,11 +50,11 @@ class DelegationService extends AgentService { s: "0", d: delegatePrefix, }; - const ixnResult = await this.signifyClient + const ixnResult = await this.props.signifyClient .identifiers() .interact(signifyName, anchor); const operation = await ixnResult.op(); - await waitAndGetDoneOp(this.signifyClient, operation); + await waitAndGetDoneOp(this.props.signifyClient, operation); return operation.done; } @@ -65,13 +65,13 @@ class DelegationService extends AgentService { if (!metadata.isPending) { return true; } - const identifier = await this.signifyClient + const identifier = await this.props.signifyClient .identifiers() .get(metadata.signifyName); - const operation = await this.signifyClient + const operation = await this.props.signifyClient .keyStates() .query(identifier.state.di, "1"); - await waitAndGetDoneOp(this.signifyClient, operation); + await waitAndGetDoneOp(this.props.signifyClient, operation); const isDone = operation.done; if (isDone) { await this.identifierStorage.updateIdentifierMetadata(metadata.id, { diff --git a/src/core/agent/services/identifierService.ts b/src/core/agent/services/identifierService.ts index c415a2e6c..892d995e6 100644 --- a/src/core/agent/services/identifierService.ts +++ b/src/core/agent/services/identifierService.ts @@ -71,7 +71,7 @@ class IdentifierService extends AgentService { if (metadata.isPending && metadata.signifyOpName) { return undefined; } - const aid = await this.signifyClient + const aid = await this.props.signifyClient .identifiers() .get(metadata.signifyName); @@ -126,13 +126,13 @@ class IdentifierService extends AgentService { ): Promise { this.validIdentifierMetadata(metadata); const signifyName = uuidv4(); - const operation = await this.signifyClient + const operation = await this.props.signifyClient .identifiers() .create(signifyName); //, this.getCreateAidOptions()); await operation.op(); - const addRoleOperation = await this.signifyClient + const addRoleOperation = await this.props.signifyClient .identifiers() - .addEndRole(signifyName, "agent", this.signifyClient.agent!.pre); + .addEndRole(signifyName, "agent", this.props.signifyClient.agent!.pre); await addRoleOperation.op(); const identifier = operation.serder.ked.i; await this.identifierStorage.createIdentifierMetadataRecord({ @@ -199,11 +199,11 @@ class IdentifierService extends AgentService { ); this.validIdentifierMetadata(metadata); - const aid = await this.signifyClient + const aid = await this.props.signifyClient .identifiers() .get(metadata.signifyName); - const manager = this.signifyClient.manager; + const manager = this.props.signifyClient.manager; if (manager) { return (await manager.get(aid)).signers[0]; } else { @@ -213,7 +213,7 @@ class IdentifierService extends AgentService { @OnlineOnly async syncKeriaIdentifiers() { - const { aids: signifyIdentifiers } = await this.signifyClient + const { aids: signifyIdentifiers } = await this.props.signifyClient .identifiers() .list(); const storageIdentifiers = @@ -253,11 +253,11 @@ class IdentifierService extends AgentService { @OnlineOnly async rotateIdentifier(metadata: IdentifierMetadataRecord) { - const rotateResult = await this.signifyClient + const rotateResult = await this.props.signifyClient .identifiers() .rotate(metadata.signifyName); const operation = await waitAndGetDoneOp( - this.signifyClient, + this.props.signifyClient, await rotateResult.op() ); if (!operation.done) { diff --git a/src/core/agent/services/ipexCommunicationService.ts b/src/core/agent/services/ipexCommunicationService.ts index 4e3e29f75..d10e5fff5 100644 --- a/src/core/agent/services/ipexCommunicationService.ts +++ b/src/core/agent/services/ipexCommunicationService.ts @@ -66,7 +66,7 @@ class IpexCommunicationService extends AgentService { waitForAcdcConfig = { maxAttempts: 120, interval: 500 } ): Promise { const notifRecord = await this.getNotificationRecordById(id); - const exn = await this.signifyClient + const exn = await this.props.signifyClient .exchanges() .get(notifRecord.a.d as string); const credentialId = exn.exn.e.acdc.d; @@ -77,7 +77,7 @@ class IpexCommunicationService extends AgentService { connectionId ); - this.eventService.emit({ + this.props.eventService.emit({ type: AcdcEventTypes.AcdcStateChanged, payload: { credentialId, @@ -108,7 +108,7 @@ class IpexCommunicationService extends AgentService { cred ); await this.notificationStorage.deleteById(id); - this.eventService.emit({ + this.props.eventService.emit({ type: AcdcEventTypes.AcdcStateChanged, payload: { status: CredentialStatus.CONFIRMED, @@ -120,19 +120,19 @@ class IpexCommunicationService extends AgentService { @OnlineOnly async offerAcdcFromApply(notification: KeriaNotification, acdc: any) { const msgSaid = notification.a.d as string; - const msg = await this.signifyClient.exchanges().get(msgSaid); + const msg = await this.props.signifyClient.exchanges().get(msgSaid); const holderSignifyName = ( await this.identifierStorage.getIdentifierMetadata(msg.exn.a.i) ).signifyName; - const [offer, sigs, end] = await this.signifyClient.ipex().offer({ + const [offer, sigs, end] = await this.props.signifyClient.ipex().offer({ senderName: holderSignifyName, recipient: msg.exn.i, acdc: new Serder(acdc), apply: msg.exn.d, }); - await this.signifyClient + await this.props.signifyClient .ipex() .submitOffer(holderSignifyName, offer, sigs, end, [msg.exn.i]); await this.notificationStorage.deleteById(notification.id); @@ -141,9 +141,11 @@ class IpexCommunicationService extends AgentService { @OnlineOnly async grantAcdcFromAgree(notification: KeriaNotification) { const msgSaid = notification.a.d as string; - const msgAgree = await this.signifyClient.exchanges().get(msgSaid); - const msgOffer = await this.signifyClient.exchanges().get(msgAgree.exn.p); - const pickedCred = await this.signifyClient + const msgAgree = await this.props.signifyClient.exchanges().get(msgSaid); + const msgOffer = await this.props.signifyClient + .exchanges() + .get(msgAgree.exn.p); + const pickedCred = await this.props.signifyClient .credentials() .get(msgOffer.exn.e.acdc.d); if (!pickedCred) { @@ -153,7 +155,7 @@ class IpexCommunicationService extends AgentService { await this.identifierStorage.getIdentifierMetadata(msgOffer.exn.i) ).signifyName; - const [grant, sigs, end] = await this.signifyClient.ipex().grant({ + const [grant, sigs, end] = await this.props.signifyClient.ipex().grant({ senderName: holderSignifyName, recipient: msgAgree.exn.i, acdc: new Serder(pickedCred.sad), @@ -163,7 +165,7 @@ class IpexCommunicationService extends AgentService { ancAttachment: pickedCred.ancatc, issAttachment: pickedCred.issAtc, }); - await this.signifyClient + await this.props.signifyClient .ipex() .submitGrant(holderSignifyName, grant, sigs, end, [msgAgree.exn.i]); await this.notificationStorage.deleteById(notification.id); @@ -193,14 +195,14 @@ class IpexCommunicationService extends AgentService { notification: KeriaNotification ): Promise { const msgSaid = notification.a.d as string; - const msg = await this.signifyClient.exchanges().get(msgSaid); + const msg = await this.props.signifyClient.exchanges().get(msgSaid); const schemaSaid = msg.exn.a.s; const attributes = msg.exn.a.a; - const schemaKeri = await this.signifyClient.schemas().get(schemaSaid); + const schemaKeri = await this.props.signifyClient.schemas().get(schemaSaid); if (!schemaKeri) { throw new Error(IpexCommunicationService.SCHEMA_NOT_FOUND); } - const creds = await this.signifyClient.credentials().list({ + const creds = await this.props.signifyClient.credentials().list({ filter: { "-s": { $eq: schemaSaid }, ...(Object.keys(attributes).length > 0 @@ -303,10 +305,10 @@ class IpexCommunicationService extends AgentService { `${ConfigurationService.env.keri.credentials.testServer.urlInt}/oobi/${IpexCommunicationService.SCHEMA_SAID_IIW_DEMO}` ); const dt = new Date().toISOString().replace("Z", "000+00:00"); - const [admit, sigs, aend] = await this.signifyClient + const [admit, sigs, aend] = await this.props.signifyClient .ipex() .admit(holderAidName, "", notificationD, dt); - await this.signifyClient + await this.props.signifyClient .ipex() .submitAdmit(holderAidName, admit, sigs, aend, [issuerAid]); } @@ -315,7 +317,7 @@ class IpexCommunicationService extends AgentService { sad: string ): Promise<{ acdc?: any; error?: unknown }> { try { - const results = await this.signifyClient.credentials().list({ + const results = await this.props.signifyClient.credentials().list({ filter: { "-d": { $eq: sad }, }, diff --git a/src/core/agent/services/multiSigService.ts b/src/core/agent/services/multiSigService.ts index d5c51e8fd..5c30f026d 100644 --- a/src/core/agent/services/multiSigService.ts +++ b/src/core/agent/services/multiSigService.ts @@ -89,7 +89,7 @@ class MultiSigService extends AgentService { if (notLinkedContacts.length) { throw new Error(MultiSigService.ONLY_ALLOW_LINKED_CONTACTS); } - const ourAid: Aid = await this.signifyClient + const ourAid: Aid = await this.props.signifyClient .identifiers() .get(ourMetadata.signifyName as string); const otherAids = await Promise.all( @@ -151,7 +151,7 @@ class MultiSigService extends AgentService { name: string; }> { const states = [aid["state"], ...otherAids.map((aid) => aid["state"])]; - const icp = await this.signifyClient.identifiers().create(name, { + const icp = await this.props.signifyClient.identifiers().create(name, { algo: Algos.group, mhab: aid, isith: threshold, @@ -212,7 +212,7 @@ class MultiSigService extends AgentService { metadata.multisigManageAid ); - const multiSig = await this.signifyClient + const multiSig = await this.props.signifyClient .identifiers() .get(metadata.signifyName); if (!multiSig) { @@ -220,7 +220,7 @@ class MultiSigService extends AgentService { } const nextSequence = (Number(multiSig.state.s) + 1).toString(); - const members = await this.signifyClient + const members = await this.props.signifyClient .identifiers() .members(metadata.signifyName); const multisigMembers = members?.signing; @@ -228,7 +228,7 @@ class MultiSigService extends AgentService { const multisigMumberAids: Aid[] = []; await Promise.allSettled( multisigMembers.map(async (signing: any) => { - const aid = await this.signifyClient + const aid = await this.props.signifyClient .keyStates() .query(signing.aid, nextSequence); if (aid.done) { @@ -239,7 +239,7 @@ class MultiSigService extends AgentService { if (multisigMembers.length !== multisigMumberAids.length) { throw new Error(MultiSigService.NOT_FOUND_ALL_MEMBER_OF_MULTISIG); } - const aid = await this.signifyClient + const aid = await this.props.signifyClient .identifiers() .get(identifierManageAid?.signifyName); @@ -255,7 +255,7 @@ class MultiSigService extends AgentService { @OnlineOnly async joinMultisigRotation(notification: KeriaNotification): Promise { const msgSaid = notification.a.d as string; - const notifications: MultiSigExnMessage[] = await this.signifyClient + const notifications: MultiSigExnMessage[] = await this.props.signifyClient .groups() .getRequest(msgSaid); if (!notifications.length) { @@ -277,7 +277,7 @@ class MultiSigService extends AgentService { multiSig.multisigManageAid ); - const aid = await this.signifyClient + const aid = await this.props.signifyClient .identifiers() .get(identifierManageAid.signifyName); const res = await this.joinMultisigRotationKeri( @@ -290,7 +290,7 @@ class MultiSigService extends AgentService { } private async hasJoinedMultisig(msgSaid: string): Promise { - const notifications: MultiSigExnMessage[] = await this.signifyClient + const notifications: MultiSigExnMessage[] = await this.props.signifyClient .groups() .getRequest(msgSaid); if (!notifications.length) { @@ -313,7 +313,7 @@ class MultiSigService extends AgentService { async getMultisigIcpDetails( notificationSaid: string ): Promise { - const icpMsg: MultiSigExnMessage[] = await this.signifyClient + const icpMsg: MultiSigExnMessage[] = await this.props.signifyClient .groups() .getRequest(notificationSaid); @@ -372,7 +372,7 @@ class MultiSigService extends AgentService { await this.notificationStorage.deleteById(notificationId); return; } - const icpMsg: MultiSigExnMessage[] = await this.signifyClient + const icpMsg: MultiSigExnMessage[] = await this.props.signifyClient .groups() .getRequest(notificationSaid); @@ -396,7 +396,7 @@ class MultiSigService extends AgentService { throw new Error(MultiSigService.MISSING_GROUP_METADATA); } - const aid = await this.signifyClient + const aid = await this.props.signifyClient .identifiers() .get(identifier?.signifyName); const signifyName = uuidv4(); @@ -431,7 +431,7 @@ class MultiSigService extends AgentService { done: true, }; } - const pendingOperation = await this.signifyClient + const pendingOperation = await this.props.signifyClient .operations() .get(metadata.signifyOpName); if (pendingOperation && pendingOperation.done) { @@ -478,7 +478,7 @@ class MultiSigService extends AgentService { icpResult: EventResult; }> { const states = [...multisigAidMembers.map((aid) => aid["state"])]; - const icp = await this.signifyClient + const icp = await this.props.signifyClient .identifiers() .rotate(name, { states: states, rstates: states }); const op = await icp.op(); @@ -528,7 +528,7 @@ class MultiSigService extends AgentService { name: string; }> { const rstates = exn.a.rstates; - const icpResult = await this.signifyClient + const icpResult = await this.props.signifyClient .identifiers() .rotate(name, { states: rstates, rstates: rstates }); const op = await icpResult.op(); @@ -570,7 +570,7 @@ class MultiSigService extends AgentService { private async getIdentifierById( id: string ): Promise { - const allIdentifiers = await this.signifyClient.identifiers().list(); + const allIdentifiers = await this.props.signifyClient.identifiers().list(); const identifier = allIdentifiers.aids.find( (identifier: IdentifierResult) => identifier.prefix === id ); @@ -591,7 +591,7 @@ class MultiSigService extends AgentService { // @TODO - foconnor: We can skip our member and get state from aid param. const states = await Promise.all( exn.a.smids.map(async (member) => { - const result = await this.signifyClient.keyStates().get(member); + const result = await this.props.signifyClient.keyStates().get(member); if (result.length === 0) { throw new Error( MultiSigService.CANNOT_GET_KEYSTATES_FOR_MULTISIG_MEMBER @@ -604,7 +604,7 @@ class MultiSigService extends AgentService { // @TODO - foconnor: Check if smids === rmids, and if so, skip this. const rstates = await Promise.all( exn.a.rmids.map(async (member) => { - const result = await this.signifyClient.keyStates().get(member); + const result = await this.props.signifyClient.keyStates().get(member); if (result.length === 0) { throw new Error( MultiSigService.CANNOT_GET_KEYSTATES_FOR_MULTISIG_MEMBER @@ -613,16 +613,18 @@ class MultiSigService extends AgentService { return result[0]; }) ); - const icpResult = await this.signifyClient.identifiers().create(name, { - algo: Algos.group, - mhab: aid, - isith: icp.kt, - nsith: icp.nt, - toad: parseInt(icp.bt), - wits: icp.b, - states, - rstates, - }); + const icpResult = await this.props.signifyClient + .identifiers() + .create(name, { + algo: Algos.group, + mhab: aid, + isith: icp.kt, + nsith: icp.nt, + toad: parseInt(icp.bt), + wits: icp.b, + states, + rstates, + }); const op = await icpResult.op(); const serder = icpResult.serder; const sigs = icpResult.sigs; @@ -678,7 +680,7 @@ class MultiSigService extends AgentService { recp: any, payload: CreateMultisigExnPayload ): Promise { - return this.signifyClient + return this.props.signifyClient .exchanges() .send(name, "multisig", aid, route, payload, embeds, recp); } diff --git a/src/core/agent/services/signifyNotificationService.ts b/src/core/agent/services/signifyNotificationService.ts index b1824e0b7..81cff2756 100644 --- a/src/core/agent/services/signifyNotificationService.ts +++ b/src/core/agent/services/signifyNotificationService.ts @@ -56,7 +56,7 @@ class SignifyNotificationService extends AgentService { let notifications; try { - notifications = await this.signifyClient + notifications = await this.props.signifyClient .notifications() .list(startFetchingIndex, startFetchingIndex + 24); } catch (error) { @@ -64,7 +64,7 @@ class SignifyNotificationService extends AgentService { // so check if its gone down to avoid having 2 bootAndConnect loops if (Agent.agent.getKeriaOnlineStatus()) { // This will hang the loop until the connection is secured again - await Agent.agent.bootAndConnect(); + await Agent.agent.connect(); } } if (!notifications) { @@ -128,7 +128,7 @@ class SignifyNotificationService extends AgentService { ) { // We only process with the credential and the multisig at the moment if (notif.a.r === NotificationRoute.MultiSigIcp) { - const multisigNotification = await this.signifyClient + const multisigNotification = await this.props.signifyClient .groups() .getRequest(notif.a.d); if (!multisigNotification || !multisigNotification.length) { @@ -173,7 +173,7 @@ class SignifyNotificationService extends AgentService { route: event.a.r, }; if (event.a.r === NotificationRoute.MultiSigIcp) { - const multisigNotification = await this.signifyClient + const multisigNotification = await this.props.signifyClient .groups() .getRequest(event.a.d); if (multisigNotification && multisigNotification.length) { @@ -209,7 +209,7 @@ class SignifyNotificationService extends AgentService { } private markNotification(notiSaid: string) { - return this.signifyClient.notifications().mark(notiSaid); + return this.props.signifyClient.notifications().mark(notiSaid); } async findNotificationsByMultisigId(multisigId: string) { diff --git a/src/core/agent/services/utils.ts b/src/core/agent/services/utils.ts index 18d45f511..f676ec8ee 100644 --- a/src/core/agent/services/utils.ts +++ b/src/core/agent/services/utils.ts @@ -48,7 +48,7 @@ export const OnlineOnly = ( /** If the error is failed to fetch with signify, * we retry until the connection is secured*/ if (errorStack && /SignifyClient/gi.test(errorStack)) { - Agent.agent.bootAndConnect(1000); + Agent.agent.connect(1000); throw new Error(Agent.KERIA_CONNECTION_BROKEN); } else { throw error; diff --git a/src/core/configuration/configurationService.ts b/src/core/configuration/configurationService.ts index 5b43de55f..c2547f05b 100644 --- a/src/core/configuration/configurationService.ts +++ b/src/core/configuration/configurationService.ts @@ -40,16 +40,18 @@ class ConfigurationService { } private setKeriaIp() { - const keriaUrl = ConfigurationService.configurationEnv.keri.keria.url; + const keriaUrl = ConfigurationService.configurationEnv.keri?.keria?.url; const keriaBootUrl = - ConfigurationService.configurationEnv.keri.keria.bootUrl; - if (keriaIP) { - ConfigurationService.configurationEnv.keri.keria.url = keriaUrl.replace( + ConfigurationService.configurationEnv.keri?.keria?.bootUrl; + if (keriaIP && ConfigurationService.configurationEnv.keri?.keria?.url) { + ConfigurationService.configurationEnv.keri.keria.url = keriaUrl?.replace( /\/\/[^:]+/, `//${keriaIP}` ); + } + if (keriaIP && ConfigurationService.configurationEnv.keri?.keria?.bootUrl) { ConfigurationService.configurationEnv.keri.keria.bootUrl = - keriaBootUrl.replace(/\/\/[^:]+/, `//${keriaIP}`); + keriaBootUrl?.replace(/\/\/[^:]+/, `//${keriaIP}`); } } @@ -61,20 +63,6 @@ class ConfigurationService { return this.invalid("Missing top-level KERI object"); } - // KERIA config - if (typeof keri.keria !== "object") { - return this.invalid("Missing KERIA config"); - } - - if ( - !( - typeof keri.keria.url === "string" && - typeof keri.keria.bootUrl === "string" - ) - ) { - return this.invalid("Missing KERIA URLs (main or boot)"); - } - // CREDENTIALS config if (typeof keri.credentials !== "object") { return this.invalid("Missing credentials config"); diff --git a/src/core/configuration/configurationService.types.ts b/src/core/configuration/configurationService.types.ts index 5fca81356..8a2cbc359 100644 --- a/src/core/configuration/configurationService.types.ts +++ b/src/core/configuration/configurationService.types.ts @@ -1,6 +1,6 @@ interface KeriaConfig { - url: string; - bootUrl: string; + url?: string; + bootUrl?: string; } interface CredentialsConfig { @@ -33,7 +33,7 @@ type BackingConfig = interface Configuration { keri: { - keria: KeriaConfig; + keria?: KeriaConfig; credentials: CredentialsConfig; backing: BackingConfig; }; diff --git a/src/locales/en/en.json b/src/locales/en/en.json index 92f459ec3..6d8d4afc1 100644 --- a/src/locales/en/en.json +++ b/src/locales/en/en.json @@ -535,6 +535,14 @@ } ] }, + "aboutssiagent": { + "done": "Done", + "intro": { + "title": "About SSI agent", + "text": "" + }, + "sections": [] + }, "lockpage": { "title": "Welcome back", "description": "Please enter your passcode to login", @@ -632,6 +640,30 @@ "hintSameAsPassword": "Your hint cannot be your password" } }, + "ssiagent": { + "title": "Enter your SSI agent details", + "description": "To continue, please enter the SSI agent boot and connect URLs (in your email or from your command line).", + "button": { + "info": "Get more information", + "validate": "Validate" + }, + "input": { + "boot": { + "label": "Boot URL", + "placeholder": "Paste or scan your boot URL" + }, + "connect": { + "label": "Connect URL", + "placeholder": "Paste or scan your connect URL" + } + }, + "error": { + "invalidurl": "Enter a valid URL", + "invalidbooturl": "Enter a valid boot URL", + "invalidconnecturl": "Enter a valid connect URL", + "mismatchconnecturl": "This connect URL doesn’t match the boot URL" + } + }, "operationspasswordregex": { "label": { "length": "8 - 64 characters long", diff --git a/src/routes/backRoute/backRoute.test.ts b/src/routes/backRoute/backRoute.test.ts index 63f266f01..6157111bd 100644 --- a/src/routes/backRoute/backRoute.test.ts +++ b/src/routes/backRoute/backRoute.test.ts @@ -22,6 +22,10 @@ describe("getBackRoute", () => { seedPhrase: "", bran: "", }, + ssiAgentCache: { + bootUrl: "", + connectUrl: "", + }, stateCache: { initialized: true, routes: [{ path: "/route1" }, { path: "/route2" }, { path: "/route3" }], @@ -33,6 +37,7 @@ describe("getBackRoute", () => { loggedIn: false, userName: "", time: 0, + ssiAgentIsSet: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { @@ -143,6 +148,10 @@ describe("getPreviousRoute", () => { seedPhrase: "", bran: "", }, + ssiAgentCache: { + bootUrl: "", + connectUrl: "", + }, stateCache: { initialized: true, routes: [{ path: "/route1" }, { path: "/route2" }, { path: "/route3" }], @@ -154,6 +163,7 @@ describe("getPreviousRoute", () => { loggedIn: false, userName: "", time: 0, + ssiAgentIsSet: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { diff --git a/src/routes/backRoute/backRoute.ts b/src/routes/backRoute/backRoute.ts index a9afbb80a..f1f870cfe 100644 --- a/src/routes/backRoute/backRoute.ts +++ b/src/routes/backRoute/backRoute.ts @@ -79,6 +79,9 @@ const backRoute: Record = { [RoutePath.VERIFY_SEED_PHRASE]: { updateRedux: [removeCurrentRoute, updateStoreSetCurrentRoute], }, + [RoutePath.SSI_AGENT]: { + updateRedux: [], + }, [RoutePath.SET_PASSCODE]: { updateRedux: [removeCurrentRoute, updateStoreSetCurrentRoute], }, diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 3aff96468..f32771248 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -19,6 +19,7 @@ import { RoutePath, TabsRoutePath } from "./paths"; import { IdentifierDetails } from "../ui/pages/IdentifierDetails"; import { CredentialDetails } from "../ui/pages/CredentialDetails"; import { ConnectionDetails } from "../ui/pages/ConnectionDetails"; +import { CreateSSIAgent } from "../ui/pages/CreateSSIAgent"; const Routes = () => { const stateCache = useAppSelector(getStateCache); @@ -72,6 +73,12 @@ const Routes = () => { exact /> + + { seedPhraseIsSet: false, passwordIsSet: false, passwordIsSkipped: true, + ssiAgentIsSet: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { @@ -66,6 +68,10 @@ describe("NextRoute", () => { biometryCache: { enabled: false, }, + ssiAgentCache: { + bootUrl: "", + connectUrl: "", + }, }; data = { store: storeMock, @@ -131,6 +137,14 @@ describe("NextRoute", () => { test("should return correct route for /verifyseedphrase", () => { const result = getNextVerifySeedPhraseRoute(); + expect(result).toEqual({ + pathname: RoutePath.SSI_AGENT, + }); + }); + + test("should return correct route for /ssiagent", () => { + const result = getNextCreateSSIAgentRoute(); + expect(result).toEqual({ pathname: RoutePath.TABS_MENU, }); @@ -150,6 +164,7 @@ describe("getNextRoute", () => { seedPhraseIsSet: false, passwordIsSet: false, passwordIsSkipped: true, + ssiAgentIsSet: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { @@ -185,6 +200,10 @@ describe("getNextRoute", () => { biometryCache: { enabled: false, }, + ssiAgentCache: { + bootUrl: "", + connectUrl: "", + }, }; const state = {}; const payload = {}; @@ -219,7 +238,7 @@ describe("getNextRoute", () => { const result = getNextSetPasscodeRoute(storeMock); expect(result).toEqual({ - pathname: RoutePath.TABS_MENU, + pathname: RoutePath.SSI_AGENT, }); }); diff --git a/src/routes/nextRoute/nextRoute.ts b/src/routes/nextRoute/nextRoute.ts index efa0fecf8..7bc153d0a 100644 --- a/src/routes/nextRoute/nextRoute.ts +++ b/src/routes/nextRoute/nextRoute.ts @@ -17,7 +17,11 @@ const getNextRootRoute = (store: StoreState) => { const authentication = store.stateCache.authentication; let path; - if (authentication.passcodeIsSet && authentication.seedPhraseIsSet) { + if ( + authentication.passcodeIsSet && + authentication.seedPhraseIsSet && + authentication.ssiAgentIsSet + ) { path = RoutePath.TABS_MENU; } else { path = RoutePath.ONBOARDING; @@ -27,13 +31,22 @@ const getNextRootRoute = (store: StoreState) => { }; const getNextOnboardingRoute = (data: DataProps) => { - let path; + let path = RoutePath.SET_PASSCODE; + if (data.store.stateCache.authentication.passcodeIsSet) { - path = data.store.stateCache.authentication.passwordIsSet - ? RoutePath.GENERATE_SEED_PHRASE - : RoutePath.CREATE_PASSWORD; - } else { - path = RoutePath.SET_PASSCODE; + path = RoutePath.CREATE_PASSWORD; + } + + if (data.store.stateCache.authentication.passwordIsSet) { + path = RoutePath.GENERATE_SEED_PHRASE; + } + + if (data.store.stateCache.authentication.seedPhraseIsSet) { + path = RoutePath.SSI_AGENT; + } + + if (data.store.stateCache.authentication.ssiAgentIsSet) { + path = RoutePath.TABS_MENU; } return { pathname: path }; @@ -57,10 +70,17 @@ const getNextCredentialDetailsRoute = () => { const getNextSetPasscodeRoute = (store: StoreState) => { const seedPhraseIsSet = !!store.seedPhraseCache?.seedPhrase; + const ssiAgentIsSet = store.stateCache.authentication.ssiAgentIsSet; - const nextPath: string = seedPhraseIsSet - ? RoutePath.TABS_MENU - : RoutePath.CREATE_PASSWORD; + let nextPath = RoutePath.CREATE_PASSWORD; + + if (seedPhraseIsSet) { + nextPath = RoutePath.SSI_AGENT; + } + + if (ssiAgentIsSet) { + nextPath = RoutePath.TABS_MENU; + } return { pathname: nextPath }; }; @@ -80,11 +100,23 @@ const updateStoreAfterVerifySeedPhraseRoute = (data: DataProps) => { }); }; +const updateStoreAfterSetupSSI = (data: DataProps) => { + return setAuthentication({ + ...data.store.stateCache.authentication, + ssiAgentIsSet: true, + }); +}; + const getNextGenerateSeedPhraseRoute = () => { return { pathname: RoutePath.VERIFY_SEED_PHRASE }; }; const getNextVerifySeedPhraseRoute = () => { + const nextPath = RoutePath.SSI_AGENT; + return { pathname: nextPath }; +}; + +const getNextCreateSSIAgentRoute = () => { const nextPath = RoutePath.TABS_MENU; return { pathname: nextPath }; }; @@ -162,6 +194,10 @@ const nextRoute: Record = { nextPath: () => getNextVerifySeedPhraseRoute(), updateRedux: [updateStoreAfterVerifySeedPhraseRoute, clearSeedPhraseCache], }, + [RoutePath.SSI_AGENT]: { + nextPath: () => getNextCreateSSIAgentRoute(), + updateRedux: [updateStoreAfterSetupSSI], + }, [RoutePath.CREATE_PASSWORD]: { nextPath: () => getNextCreatePasswordRoute(), updateRedux: [updateStoreAfterCreatePassword], @@ -197,4 +233,5 @@ export { updateStoreAfterVerifySeedPhraseRoute, getNextCreatePasswordRoute, updateStoreAfterCreatePassword, + getNextCreateSSIAgentRoute, }; diff --git a/src/routes/paths.ts b/src/routes/paths.ts index ea0047e4c..02626ef40 100644 --- a/src/routes/paths.ts +++ b/src/routes/paths.ts @@ -6,6 +6,7 @@ enum RoutePath { VERIFY_SEED_PHRASE = "/verifyseedphrase", TABS_MENU = "/tabs", CREATE_PASSWORD = "/createpassword", + SSI_AGENT = "/ssiagent", CONNECTION_DETAILS = "/connectiondetails", } diff --git a/src/store/index.ts b/src/store/index.ts index f5a9101c8..5f05b6b1f 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -5,11 +5,9 @@ import { identifiersCacheSlice } from "./reducers/identifiersCache"; import { credsCacheSlice } from "./reducers/credsCache"; import { connectionsCacheSlice } from "./reducers/connectionsCache"; import { walletConnectionsCacheSlice } from "./reducers/walletConnectionsCache"; -import { - getIdentifierViewTypeCacheCache, - identifierViewTypeCacheSlice, -} from "./reducers/identifierViewTypeCache"; +import { identifierViewTypeCacheSlice } from "./reducers/identifierViewTypeCache"; import { biometryCacheSlice } from "./reducers/biometryCache"; +import { ssiAgentSlice } from "./reducers/ssiAgent"; const store = configureStore({ reducer: { @@ -21,6 +19,7 @@ const store = configureStore({ walletConnectionsCache: walletConnectionsCacheSlice.reducer, identifierViewTypeCacheCache: identifierViewTypeCacheSlice.reducer, biometryCache: biometryCacheSlice.reducer, + ssiAgentCache: ssiAgentSlice.reducer, }, }); diff --git a/src/store/reducers/ssiAgent/index.ts b/src/store/reducers/ssiAgent/index.ts new file mode 100644 index 000000000..1d392c6e2 --- /dev/null +++ b/src/store/reducers/ssiAgent/index.ts @@ -0,0 +1 @@ +export * from "./ssiAgent"; diff --git a/src/store/reducers/ssiAgent/ssiAgent.test.ts b/src/store/reducers/ssiAgent/ssiAgent.test.ts new file mode 100644 index 000000000..6ecd7e4ba --- /dev/null +++ b/src/store/reducers/ssiAgent/ssiAgent.test.ts @@ -0,0 +1,73 @@ +import { PayloadAction } from "@reduxjs/toolkit"; +import { + clearSSIAgent, + getSSIAgent, + setBootUrl, + setConnectUrl, + setSSIAgent, + ssiAgentSlice, +} from "./ssiAgent"; +import { SSIAgentState } from "./ssiAgent.types"; +import { RootState } from "../.."; + +describe("SSI Agent Cache", () => { + const initialState: SSIAgentState = { + bootUrl: undefined, + connectUrl: undefined, + }; + + test("should return the initial state on first run", () => { + expect(ssiAgentSlice.reducer(undefined, {} as PayloadAction)).toEqual( + initialState + ); + }); + + it("should handle setSSIAgent", () => { + const ssiAgent: SSIAgentState = { + bootUrl: "boot url", + connectUrl: "connect url", + }; + const newState = ssiAgentSlice.reducer(initialState, setSSIAgent(ssiAgent)); + expect(newState).toEqual(ssiAgent); + }); + + it("should handle setBootUrl", () => { + const bootUrl = "boot url"; + const newState = ssiAgentSlice.reducer(initialState, setBootUrl(bootUrl)); + expect(newState.bootUrl).toEqual(bootUrl); + }); + + it("should handle setBootUrl", () => { + const connectUrl = "connect url"; + const newState = ssiAgentSlice.reducer( + initialState, + setConnectUrl(connectUrl) + ); + expect(newState.connectUrl).toEqual(connectUrl); + }); + + it("should handle setConnectUrl", () => { + const ssiAgent: SSIAgentState = { + bootUrl: "boot url", + connectUrl: "connect url", + }; + const newState = ssiAgentSlice.reducer(initialState, setSSIAgent(ssiAgent)); + expect(newState).toEqual(ssiAgent); + }); + + it("should handle clearSsiAgent", () => { + const newState = ssiAgentSlice.reducer(initialState, clearSSIAgent()); + expect(newState).toEqual(initialState); + }); + + it("should ssi agent from RootState", () => { + const state = { + ssiAgentCache: { + bootUrl: "boot url", + connectUrl: "connect url", + }, + } as RootState; + const ssiAgent = getSSIAgent(state); + expect(ssiAgent).toEqual(state.ssiAgentCache); + }); +}); diff --git a/src/store/reducers/ssiAgent/ssiAgent.ts b/src/store/reducers/ssiAgent/ssiAgent.ts new file mode 100644 index 000000000..feccc8b1f --- /dev/null +++ b/src/store/reducers/ssiAgent/ssiAgent.ts @@ -0,0 +1,43 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { RootState } from "../../index"; +import { SSIAgentState } from "./ssiAgent.types"; + +const initialState: SSIAgentState = { + connectUrl: undefined, + bootUrl: undefined, +}; + +const ssiAgentSlice = createSlice({ + name: "ssiAgent", + initialState, + reducers: { + setSSIAgent: (state, action: PayloadAction) => { + state.connectUrl = action.payload.connectUrl; + state.bootUrl = action.payload.bootUrl; + }, + setConnectUrl: (state, action: PayloadAction) => { + state.connectUrl = action.payload; + }, + setBootUrl: (state, action: PayloadAction) => { + state.bootUrl = action.payload; + }, + clearSSIAgent: (state) => { + state.connectUrl = undefined; + state.bootUrl = undefined; + }, + }, +}); + +const { setSSIAgent, clearSSIAgent, setBootUrl, setConnectUrl } = + ssiAgentSlice.actions; + +const getSSIAgent = (state: RootState) => state.ssiAgentCache; + +export { + clearSSIAgent, + getSSIAgent, + setSSIAgent, + ssiAgentSlice, + setBootUrl, + setConnectUrl, +}; diff --git a/src/store/reducers/ssiAgent/ssiAgent.types.ts b/src/store/reducers/ssiAgent/ssiAgent.types.ts new file mode 100644 index 000000000..4f732708d --- /dev/null +++ b/src/store/reducers/ssiAgent/ssiAgent.types.ts @@ -0,0 +1,6 @@ +interface SSIAgentState { + connectUrl?: string; + bootUrl?: string; +} + +export type { SSIAgentState }; diff --git a/src/store/reducers/stateCache/stateCache.test.ts b/src/store/reducers/stateCache/stateCache.test.ts index 8b2647e81..7d6286812 100644 --- a/src/store/reducers/stateCache/stateCache.test.ts +++ b/src/store/reducers/stateCache/stateCache.test.ts @@ -56,6 +56,7 @@ describe("State Cache", () => { seedPhraseIsSet: false, passwordIsSet: false, passwordIsSkipped: false, + ssiAgentIsSet: false, }; const action = setAuthentication(authentication); const nextState = stateCacheSlice.reducer(initialState, action); diff --git a/src/store/reducers/stateCache/stateCache.ts b/src/store/reducers/stateCache/stateCache.ts index 31bdfdb5d..160d14b19 100644 --- a/src/store/reducers/stateCache/stateCache.ts +++ b/src/store/reducers/stateCache/stateCache.ts @@ -20,6 +20,7 @@ const initialState: StateCacheProps = { seedPhraseIsSet: false, passwordIsSet: false, passwordIsSkipped: true, + ssiAgentIsSet: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { diff --git a/src/store/reducers/stateCache/stateCache.types.ts b/src/store/reducers/stateCache/stateCache.types.ts index 00ee53a29..c4a42981d 100644 --- a/src/store/reducers/stateCache/stateCache.types.ts +++ b/src/store/reducers/stateCache/stateCache.types.ts @@ -20,6 +20,7 @@ interface AuthenticationCacheProps { seedPhraseIsSet: boolean; passwordIsSet: boolean; passwordIsSkipped: boolean; + ssiAgentIsSet: boolean; } enum IncomingRequestType { CREDENTIAL_OFFER_RECEIVED = "credential-offer-received", diff --git a/src/ui/App.test.tsx b/src/ui/App.test.tsx index d93f55319..5173bc572 100644 --- a/src/ui/App.test.tsx +++ b/src/ui/App.test.tsx @@ -12,6 +12,7 @@ jest.mock("../core/agent/agent", () => ({ Agent: { agent: { start: jest.fn(), + initDatabaseConnection: jest.fn(), identifiers: { getIdentifiers: jest.fn().mockResolvedValue([]), syncKeriaIdentifiers: jest.fn(), diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 4680e7965..16c583a6e 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -69,6 +69,8 @@ const App = () => { OperationType.SCAN_WALLET_CONNECTION, OperationType.MULTI_SIG_INITIATOR_SCAN, OperationType.MULTI_SIG_RECEIVER_SCAN, + OperationType.SCAN_SSI_BOOT_URL, + OperationType.SCAN_SSI_CONNECT_URL, ].includes(currentOperation) ); setShowToast(toastMsg !== undefined); diff --git a/src/ui/components/AppWrapper/AppWrapper.test.tsx b/src/ui/components/AppWrapper/AppWrapper.test.tsx index d95c2ce95..6d3cd79de 100644 --- a/src/ui/components/AppWrapper/AppWrapper.test.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.test.tsx @@ -52,6 +52,7 @@ jest.mock("../../../core/agent/agent", () => ({ Agent: { agent: { start: jest.fn(), + initDatabaseConnection: jest.fn(), identifiers: { getIdentifiers: jest.fn().mockResolvedValue([]), syncKeriaIdentifiers: jest.fn(), diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index d5d8073ee..9810444c5 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -273,6 +273,9 @@ const AppWrapper = (props: { children: ReactNode }) => { const seedPhraseIsSet = await checkKeyStore(KeyStoreKeys.SIGNIFY_BRAN); const passwordIsSet = await checkKeyStore(KeyStoreKeys.APP_OP_PASSWORD); + const keriaConnectUrlRecord = await Agent.agent.basicStorage.findById( + MiscRecordId.KERIA_CONNECT_URL + ); const identifiersFavourites = await Agent.agent.basicStorage.findById( MiscRecordId.IDENTIFIERS_FAVOURITES @@ -321,33 +324,19 @@ const AppWrapper = (props: { children: ReactNode }) => { passcodeIsSet, seedPhraseIsSet, passwordIsSet, + ssiAgentIsSet: + !!keriaConnectUrlRecord && !!keriaConnectUrlRecord.content.url, }) ); + + return { + keriaConnectUrlRecord, + }; }; const initApp = async () => { await new ConfigurationService().start(); - try { - await Agent.agent.start(); - setIsOnline(true); - await loadDatabase(); - } catch (e) { - const errorStack = (e as Error).stack as string; - const errorMessage = (e as Error).message; - - // If the error is failed to fetch with signify, we retry until the connection is secured - if (/SignifyClient/gi.test(errorStack)) { - await loadDatabase(); - Agent.agent.bootAndConnect().then(() => { - setIsOnline(Agent.agent.getKeriaOnlineStatus()); - }); - } else if (/signify-bran/gi.test(errorMessage)) { - dispatch(setInitialized(true)); - return; - } else { - throw e; - } - } + await Agent.agent.initDatabaseConnection(); // @TODO - foconnor: This is a temp hack for development to be removed pre-release. // These items are removed from the secure storage on re-install to re-test the on-boarding for iOS devices. const appAlreadyInit = await Agent.agent.basicStorage.findById( @@ -358,7 +347,25 @@ const AppWrapper = (props: { children: ReactNode }) => { await SecureStorage.delete(KeyStoreKeys.APP_OP_PASSWORD); await SecureStorage.delete(KeyStoreKeys.SIGNIFY_BRAN); } - await loadCacheBasicStorage(); + await loadDatabase(); + const { keriaConnectUrlRecord } = await loadCacheBasicStorage(); + + if (keriaConnectUrlRecord) { + try { + await Agent.agent.start(keriaConnectUrlRecord.content.url as string); + setIsOnline(true); + } catch (e) { + const errorStack = (e as Error).stack as string; + // If the error is failed to fetch with signify, we retry until the connection is secured + if (/SignifyClient/gi.test(errorStack)) { + Agent.agent.connect().then(() => { + setIsOnline(Agent.agent.getKeriaOnlineStatus()); + }); + } else { + throw e; + } + } + } Agent.agent.onKeriaStatusStateChanged((event) => { setIsOnline(event.payload.isOnline); diff --git a/src/ui/components/CustomInput/CustomInput.tsx b/src/ui/components/CustomInput/CustomInput.tsx index 859033238..1670f2c15 100644 --- a/src/ui/components/CustomInput/CustomInput.tsx +++ b/src/ui/components/CustomInput/CustomInput.tsx @@ -16,6 +16,8 @@ const CustomInput = ({ optional, value, error, + action, + actionIcon, }: CustomInputProps) => { const [hidden, setHidden] = useState(hiddenInput); @@ -66,6 +68,21 @@ const CustomInput = ({ /> )} + {action && actionIcon && ( + { + action(e); + }} + > + + + )} ); diff --git a/src/ui/components/CustomInput/CustomInput.types.ts b/src/ui/components/CustomInput/CustomInput.types.ts index e8479a52e..b12fbfb92 100644 --- a/src/ui/components/CustomInput/CustomInput.types.ts +++ b/src/ui/components/CustomInput/CustomInput.types.ts @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction } from "react"; +import { Dispatch, SetStateAction, MouseEvent as ReactMouseEvent } from "react"; interface CustomInputProps { dataTestId: string; @@ -11,6 +11,8 @@ interface CustomInputProps { onChangeFocus?: Dispatch>; optional?: boolean; error?: boolean; + actionIcon?: string; + action?: (e: ReactMouseEvent) => void; } export type { CustomInputProps }; diff --git a/src/ui/components/Scanner/Scanner.tsx b/src/ui/components/Scanner/Scanner.tsx index 561f6d3e5..c6195ad6b 100644 --- a/src/ui/components/Scanner/Scanner.tsx +++ b/src/ui/components/Scanner/Scanner.tsx @@ -37,6 +37,7 @@ import { CustomInput } from "../CustomInput"; import { OptionModal } from "../OptionsModal"; import { setPendingDAppMeerKat } from "../../../store/reducers/walletConnectionsCache"; import { CreateIdentifier } from "../CreateIdentifier"; +import { setBootUrl, setConnectUrl } from "../../../store/reducers/ssiAgent"; const Scanner = forwardRef( ({ setIsValueCaptured, handleReset }: ScannerProps, ref) => { @@ -124,6 +125,24 @@ const Scanner = forwardRef( return; } + if ( + [ + OperationType.SCAN_SSI_BOOT_URL, + OperationType.SCAN_SSI_CONNECT_URL, + ].includes(currentOperation) + ) { + if (OperationType.SCAN_SSI_BOOT_URL === currentOperation) { + dispatch(setBootUrl(content)); + } + + if (OperationType.SCAN_SSI_CONNECT_URL === currentOperation) { + dispatch(setConnectUrl(content)); + } + + handleReset && handleReset(); + return; + } + // @TODO - foconnor: when above loading screen in place, handle invalid QR code const invitation = await Agent.agent.connections.connectByOobiUrl( content @@ -167,6 +186,8 @@ const Scanner = forwardRef( [ OperationType.SCAN_CONNECTION, OperationType.SCAN_WALLET_CONNECTION, + OperationType.SCAN_SSI_BOOT_URL, + OperationType.SCAN_SSI_CONNECT_URL, ].includes(currentOperation)) && currentToastMsg !== ToastMsgType.CONNECTION_REQUEST_PENDING && currentToastMsg !== ToastMsgType.CREDENTIAL_REQUEST_PENDING) || @@ -224,6 +245,9 @@ const Scanner = forwardRef( secondaryButtonAction={() => setPasteModalIsOpen(true)} /> ); + case OperationType.SCAN_SSI_BOOT_URL: + case OperationType.SCAN_SSI_CONNECT_URL: + return
; default: return ( * { + margin: 0.5rem 0 0; + } + + & > ion-item { + margin-top: 0; + } + + & > .page-footer { + margin-top: 1rem; + } + } + + .error-message-placeholder { + display: none; + } + + .error-message { + text-align: left; + + > p { + margin: 0; + } + } + + .page-header { + ion-toolbar { + --background: transparent; + } + } +} + +.ssi-spinner-container { + position: absolute; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + display: flex; + justify-content: center; + align-items: center; + z-index: 101; +} diff --git a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx new file mode 100644 index 000000000..d44d548a1 --- /dev/null +++ b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx @@ -0,0 +1,322 @@ +import { Provider } from "react-redux"; +import { fireEvent, render, waitFor } from "@testing-library/react"; +import configureStore from "redux-mock-store"; +import { IonButton, IonIcon, IonInput, IonLabel } from "@ionic/react"; +import { act } from "react-dom/test-utils"; +import { ionFireEvent } from "@ionic/react-test-utils"; +import { IonReactMemoryRouter } from "@ionic/react-router"; +import { createMemoryHistory } from "history"; +import { CreateSSIAgent } from "./CreateSSIAgent"; +import ENG_Trans from "../../../locales/en/en.json"; +import { CustomInputProps } from "../../components/CustomInput/CustomInput.types"; +import { setCurrentOperation } from "../../../store/reducers/stateCache"; +import { OperationType } from "../../globals/types"; +import { setBootUrl, setConnectUrl } from "../../../store/reducers/ssiAgent"; +import { RoutePath } from "../../../routes"; +import { Agent } from "../../../core/agent/agent"; + +const bootAndConnectMock = jest.fn((...args: any) => Promise.resolve()); + +jest.mock("../../../core/agent/agent", () => ({ + Agent: { + ...jest.requireActual("../../../core/agent/agent"), + agent: { + bootAndConnect: (...args: any) => bootAndConnectMock(...args), + }, + }, +})); + +jest.mock("../../components/CustomInput", () => ({ + CustomInput: (props: CustomInputProps) => { + return ( + <> + + {props.title} + {props.optional && ( + (optional) + )} + + { + props.onChangeInput(e.detail.value as string); + }} + onIonFocus={() => props.onChangeFocus?.(true)} + onIonBlur={() => props.onChangeFocus?.(false)} + /> + {props.action && props.actionIcon && ( + { + props.action?.(e); + }} + > + + + )} + + ); + }, +})); + +describe("SSI agent page", () => { + const mockStore = configureStore(); + const dispatchMock = jest.fn(); + const initialState = { + ssiAgentCache: { + bootUrl: undefined, + connectUrl: undefined, + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + test("Renders ssi agent page", () => { + const { getByText, getByTestId } = render( + + + + ); + + expect(getByText(ENG_Trans.ssiagent.title)).toBeVisible(); + expect(getByText(ENG_Trans.ssiagent.description)).toBeVisible(); + expect(getByText(ENG_Trans.ssiagent.button.info)).toBeVisible(); + expect(getByText(ENG_Trans.ssiagent.button.validate)).toBeVisible(); + expect( + getByText(ENG_Trans.ssiagent.button.validate).getAttribute("disabled") + ).toBe("true"); + + expect(getByTestId("boot-url-input")).toBeVisible(); + expect(getByTestId("connect-url-input")).toBeVisible(); + }); + + test("Open scanner", () => { + const { getByTestId } = render( + + + + ); + + act(() => { + fireEvent.click(getByTestId("boot-url-input-action")); + }); + + expect(dispatchMock).toBeCalledWith( + setCurrentOperation(OperationType.SCAN_SSI_BOOT_URL) + ); + + act(() => { + fireEvent.click(getByTestId("connect-url-input-action")); + }); + + expect(dispatchMock).toBeCalledWith( + setCurrentOperation(OperationType.SCAN_SSI_CONNECT_URL) + ); + }); + + test("Change store after input url", async () => { + const { getByTestId } = render( + + + + ); + + act(() => { + ionFireEvent.ionInput(getByTestId("boot-url-input"), "11111"); + }); + + await waitFor(() => { + expect(dispatchMock).toBeCalledWith(setBootUrl("11111")); + }); + + act(() => { + ionFireEvent.ionInput(getByTestId("connect-url-input"), "11111"); + }); + + await waitFor(() => { + expect(dispatchMock).toBeCalledWith(setConnectUrl("11111")); + }); + }); + + test("Display error when input invalid boot url", async () => { + const mockStore = configureStore(); + const initialState = { + ssiAgentCache: { + bootUrl: "11111", + connectUrl: undefined, + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + const { getByTestId, getByText } = render( + + + + ); + + act(() => { + ionFireEvent.ionFocus(getByTestId("boot-url-input")); + }); + + await waitFor(() => { + expect(getByText(ENG_Trans.ssiagent.error.invalidbooturl)).toBeVisible(); + }); + }); + + test("Display error when input invalid connect url", async () => { + const mockStore = configureStore(); + const initialState = { + ssiAgentCache: { + bootUrl: undefined, + connectUrl: "11111", + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + const { getByTestId, getByText } = render( + + + + ); + + act(() => { + ionFireEvent.ionFocus(getByTestId("connect-url-input")); + }); + + await waitFor(() => { + expect( + getByText(ENG_Trans.ssiagent.error.invalidconnecturl) + ).toBeVisible(); + }); + }); + + test("Remove last slash", async () => { + const mockStore = configureStore(); + const initialState = { + ssiAgentCache: { + bootUrl: undefined, + connectUrl: "https://connectUrl.com/", + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + const { getByTestId, getByText } = render( + + + + ); + + act(() => { + ionFireEvent.ionBlur(getByTestId("connect-url-input")); + }); + + await waitFor(() => { + expect(dispatchMock).toBeCalledWith( + setConnectUrl("https://connectUrl.com") + ); + }); + }); + + test("Display error when input invalid urls", async () => { + const mockStore = configureStore(); + const initialState = { + ssiAgentCache: { + bootUrl: "11111", + connectUrl: "11111", + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + const { getByTestId, getAllByText } = render( + + + + ); + + act(() => { + ionFireEvent.ionFocus(getByTestId("boot-url-input")); + ionFireEvent.ionFocus(getByTestId("connect-url-input")); + }); + + await waitFor(() => { + expect(getAllByText(ENG_Trans.ssiagent.error.invalidurl).length).toBe(2); + }); + }); + + test("Connect and boot success", async () => { + const mockStore = configureStore(); + const initialState = { + stateCache: { + authentication: { + passcodeIsSet: true, + seedPhraseIsSet: true, + passwordIsSet: true, + passwordIsSkipped: true, + loggedIn: false, + userName: "", + time: 0, + ssiAgentIsSet: false, + }, + }, + ssiAgentCache: { + bootUrl: + "https://dev.keria-boot.cf-keripy.metadata.dev.cf-deployments.org", + connectUrl: + "https://dev.keria.cf-keripy.metadata.dev.cf-deployments.org", + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + const history = createMemoryHistory(); + history.push(RoutePath.SSI_AGENT); + + const { getByTestId } = render( + + + + + + ); + + act(() => { + fireEvent.click(getByTestId("primary-button-create-ssi-agent")); + }); + + expect(bootAndConnectMock).toBeCalledWith({ + bootUrl: + "https://dev.keria-boot.cf-keripy.metadata.dev.cf-deployments.org", + url: "https://dev.keria.cf-keripy.metadata.dev.cf-deployments.org", + }); + }); +}); diff --git a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx new file mode 100644 index 000000000..efd4f8351 --- /dev/null +++ b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx @@ -0,0 +1,290 @@ +import { IonButton, IonIcon, IonSpinner } from "@ionic/react"; +import { informationCircleOutline, scanOutline } from "ionicons/icons"; +import { + useState, + MouseEvent as ReactMouseEvent, + useMemo, + useEffect, +} from "react"; +import { i18n } from "../../../i18n"; +import { RoutePath } from "../../../routes"; +import { getNextRoute } from "../../../routes/nextRoute"; +import { useAppDispatch, useAppSelector } from "../../../store/hooks"; +import { + getStateCache, + setCurrentOperation, +} from "../../../store/reducers/stateCache"; +import { updateReduxState } from "../../../store/utils"; +import { CustomInput } from "../../components/CustomInput"; +import { ErrorMessage } from "../../components/ErrorMessage"; +import { PageFooter } from "../../components/PageFooter"; +import { PageHeader } from "../../components/PageHeader"; +import { OperationType } from "../../globals/types"; +import { useAppIonRouter } from "../../hooks"; +import "./CreateSSIAgent.scss"; +import { + clearSSIAgent, + getSSIAgent, + setBootUrl, + setConnectUrl, +} from "../../../store/reducers/ssiAgent"; +import { isValidHttpUrl } from "../../utils/urlChecker"; +import { TermsModal } from "../../components/TermsModal"; +import { Agent } from "../../../core/agent/agent"; +import { ScrollablePageLayout } from "../../components/layout/ScrollablePageLayout"; +import { ConfigurationService } from "../../../core/configuration"; + +const SSI_URLS_EMPTY = "SSI url is empty"; + +const InputError = ({ + showError, + errorMessage, +}: { + showError: boolean; + errorMessage: string; +}) => { + return showError ? ( + + ) : ( +
+ ); +}; + +const CreateSSIAgent = () => { + const pageId = "create-ssi-agent"; + const ssiAgent = useAppSelector(getSSIAgent); + + const stateCache = useAppSelector(getStateCache); + const ionRouter = useAppIonRouter(); + const dispatch = useAppDispatch(); + const [connectUrlInputTouched, setConnectUrlTouched] = useState(false); + const [bootUrlInputTouched, setBootUrlInputTouched] = useState(false); + const [openInfo, setOpenInfo] = useState(false); + const [loading, setLoading] = useState(false); + const [hasMismatchError, setHasMismatchError] = useState(false); + const [isInvalidBootUrl, setIsInvalidBootUrl] = useState(false); + + useEffect(() => { + if (!ssiAgent.bootUrl && !ssiAgent.connectUrl) { + dispatch( + setConnectUrl(ConfigurationService.env?.keri?.keria?.url || undefined) + ); + dispatch( + setBootUrl(ConfigurationService.env?.keri?.keria?.bootUrl || undefined) + ); + } + }, []); + + const setTouchedConnectUrlInput = () => { + setConnectUrlTouched(true); + }; + + const setTouchedBootUrlInput = () => { + setBootUrlInputTouched(true); + }; + + const validBootUrl = useMemo(() => { + return ssiAgent.bootUrl && isValidHttpUrl(ssiAgent.bootUrl); + }, [ssiAgent]); + + const validConnectUrl = useMemo(() => { + return ssiAgent.connectUrl && isValidHttpUrl(ssiAgent.connectUrl); + }, [ssiAgent]); + + const displayBootUrlError = + bootUrlInputTouched && + ssiAgent.bootUrl && + !isValidHttpUrl(ssiAgent.bootUrl); + + const displayConnectUrlError = + connectUrlInputTouched && + ssiAgent.connectUrl && + !isValidHttpUrl(ssiAgent.connectUrl); + + const validated = validBootUrl && validConnectUrl && !hasMismatchError; + + const handleClearState = () => { + dispatch(clearSSIAgent()); + }; + + const handleValidate = async () => { + setLoading(true); + try { + if (!ssiAgent.bootUrl || !ssiAgent.connectUrl) { + throw new Error(SSI_URLS_EMPTY); + } + + await Agent.agent.bootAndConnect({ + bootUrl: ssiAgent.bootUrl, + url: ssiAgent.connectUrl, + }); + + const { nextPath, updateRedux } = getNextRoute(RoutePath.SSI_AGENT, { + store: { stateCache }, + }); + + updateReduxState( + nextPath.pathname, + { + store: { stateCache }, + }, + dispatch, + updateRedux + ); + + ionRouter.push(nextPath.pathname, "forward", "push"); + handleClearState(); + } catch (e) { + const errorMessage = (e as Error).message; + if (Agent.KERIA_BOOTED_ALREADY_BUT_CANNOT_CONNECT === errorMessage) { + setHasMismatchError(true); + } + + if (Agent.KERIA_BOOT_FAILED === errorMessage) { + setIsInvalidBootUrl(true); + } + } finally { + setLoading(false); + } + }; + + const scanBootUrl = (event: ReactMouseEvent) => { + event.stopPropagation(); + dispatch(setCurrentOperation(OperationType.SCAN_SSI_BOOT_URL)); + }; + + const scanConnectUrl = (event: ReactMouseEvent) => { + event.stopPropagation(); + dispatch(setCurrentOperation(OperationType.SCAN_SSI_CONNECT_URL)); + }; + + const removeLastSlash = (url: string) => { + let result = url; + + while (result && result.length > 0 && url[result.length - 1] === "/") { + result = result.substring(0, result.length - 1); + } + + return result; + }; + + return ( + <> + + } + > +

{i18n.t("ssiagent.title")}

+

+ {i18n.t("ssiagent.description")} +

+
+ setOpenInfo(true)} + > + + {i18n.t("ssiagent.button.info")} + +
+ { + setIsInvalidBootUrl(false); + dispatch(setBootUrl(bootUrl)); + }} + value={ssiAgent.bootUrl || ""} + onChangeFocus={(result) => { + setTouchedBootUrlInput(); + + if (!result && ssiAgent.bootUrl) { + dispatch(setBootUrl(removeLastSlash(ssiAgent.bootUrl.trim()))); + } + }} + error={!!displayBootUrlError || isInvalidBootUrl} + /> + + { + setHasMismatchError(false); + dispatch(setConnectUrl(connectionUrl)); + }} + onChangeFocus={(result) => { + setTouchedConnectUrlInput(); + + if (!result && ssiAgent.connectUrl) { + dispatch( + setConnectUrl(removeLastSlash(ssiAgent.connectUrl.trim())) + ); + } + }} + value={ssiAgent.connectUrl || ""} + error={!!displayConnectUrlError || hasMismatchError} + /> + + handleValidate()} + primaryButtonDisabled={!validated} + /> +
+ {loading && ( +
+ +
+ )} + + + ); +}; + +export { CreateSSIAgent }; diff --git a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.types.ts b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.types.ts new file mode 100644 index 000000000..82ea8752b --- /dev/null +++ b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.types.ts @@ -0,0 +1,9 @@ +interface RegexItemProps { + condition: boolean; + label: string; +} +interface PasswordRegexProps { + password: string; +} + +export type { PasswordRegexProps, RegexItemProps }; diff --git a/src/ui/pages/CreateSSIAgent/index.ts b/src/ui/pages/CreateSSIAgent/index.ts new file mode 100644 index 000000000..021d0986d --- /dev/null +++ b/src/ui/pages/CreateSSIAgent/index.ts @@ -0,0 +1 @@ +export * from "./CreateSSIAgent"; diff --git a/src/ui/utils/urlChecker.test.ts b/src/ui/utils/urlChecker.test.ts new file mode 100644 index 000000000..0f15cbdeb --- /dev/null +++ b/src/ui/utils/urlChecker.test.ts @@ -0,0 +1,31 @@ +import { isValidHttpUrl } from "./urlChecker"; + +const validUrl = [ + "https://a.long.sub-domain.example.com/foo/bar?foo=bar&boo=far#a%20b", + "www.police.academy", + "https://x.com/?twitter?", + "http://example.com?a=%bc&d=%ef&g=%H", + "https://12.34.56.78:9000", + "www.12.32.44.22:9323", +]; + +const inValidUrl = [ + "https://a", + "//x.com/?twitter?", + "dsadasda", + "3213.323.321.333", +]; + +describe("Url checker", () => { + test("Valid format", () => { + validUrl.forEach((url) => { + expect(isValidHttpUrl(url)).toBe(true); + }); + }); + + test("Invalid format", () => { + inValidUrl.forEach((url) => { + expect(isValidHttpUrl(url)).toBe(false); + }); + }); +}); diff --git a/src/ui/utils/urlChecker.ts b/src/ui/utils/urlChecker.ts new file mode 100644 index 000000000..3db22b8d1 --- /dev/null +++ b/src/ui/utils/urlChecker.ts @@ -0,0 +1,10 @@ +const isValidHttpUrl = (urlString: string) => { + const urlPattern = + /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/; + + const ipPattern = /^((https?:\/\/)|(www.))(\d+\.\d+\.\d+\.\d+)(:\d{1,5})?$/; + + return !!urlPattern.test(urlString) || ipPattern.test(urlString); +}; + +export { isValidHttpUrl }; From 1499b2623bbf3af0a6da6d789ad1a8e668b9b8ba Mon Sep 17 00:00:00 2001 From: Bao Hoang <142210850+bao-sotatek@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:00:54 +0700 Subject: [PATCH 06/28] feat(core): long running operation tracker for witnessing (#497) * feat: add func to long running operation tracker for witnessing * chore: add unittest * feat: add operation pending record * fix: change unuittest * test: add unittest * feat: update redux and some type name * test: add unittest * feat: update timeout max 2000s with creating aid * feat: move pending operation to simple variable * chore: cut recordId from oid * fix: unittest * chore: change name oid * chore: check keria offline * chore: catch operation when get error --- src/core/agent/agent.ts | 10 +- src/core/agent/records/index.ts | 1 + .../records/operationPendingRecord.test.ts | 39 +++++ .../agent/records/operationPendingRecord.ts | 36 +++++ .../records/operationPendingRecord.type.ts | 3 + .../records/operationPendingStorage.test.ts | 144 ++++++++++++++++++ .../agent/records/operationPendingStorage.ts | 36 +++++ src/core/agent/services/identifier.types.ts | 1 + .../agent/services/identifierService.test.ts | 76 +++++++-- src/core/agent/services/identifierService.ts | 33 +++- .../signifyNotificationService.test.ts | 7 +- .../services/signifyNotificationService.ts | 97 +++++++++++- .../identifiersCache/identifiersCache.test.ts | 62 ++++++++ .../identifiersCache/identifiersCache.ts | 26 ++++ src/ui/App.test.tsx | 1 + .../components/AppWrapper/AppWrapper.test.tsx | 33 +++- src/ui/components/AppWrapper/AppWrapper.tsx | 20 +++ .../components/IdentifierStage0.tsx | 4 +- .../IncomingRequest/IncomingRequest.test.tsx | 17 ++- 19 files changed, 623 insertions(+), 23 deletions(-) create mode 100644 src/core/agent/records/operationPendingRecord.test.ts create mode 100644 src/core/agent/records/operationPendingRecord.ts create mode 100644 src/core/agent/records/operationPendingRecord.type.ts create mode 100644 src/core/agent/records/operationPendingStorage.test.ts create mode 100644 src/core/agent/records/operationPendingStorage.ts diff --git a/src/core/agent/agent.ts b/src/core/agent/agent.ts index 4e8aa04f0..d724a9ccc 100644 --- a/src/core/agent/agent.ts +++ b/src/core/agent/agent.ts @@ -47,6 +47,8 @@ import { IonicStorage } from "../storage/ionicStorage"; import { SqliteStorage } from "../storage/sqliteStorage"; import { BaseRecord } from "../storage/storage.types"; import { ConfigurationService } from "../configuration"; +import { OperationPendingStorage } from "./records/operationPendingStorage"; +import { OperationPendingRecord } from "./records/operationPendingRecord"; const walletId = "idw"; class Agent { @@ -71,6 +73,7 @@ class Agent { private connectionNoteStorage!: ConnectionNoteStorage; private notificationStorage!: NotificationStorage; private peerConnectionStorage!: PeerConnectionStorage; + private operationPendingStorage!: OperationPendingStorage; private signifyClient!: SignifyClient; @@ -88,7 +91,8 @@ class Agent { if (!this.identifierService) { this.identifierService = new IdentifierService( this.agentServicesProps, - this.identifierStorage + this.identifierStorage, + this.operationPendingStorage ); } return this.identifierService; @@ -152,7 +156,9 @@ class Agent { if (!this.signifyNotificationService) { this.signifyNotificationService = new SignifyNotificationService( this.agentServicesProps, - this.notificationStorage + this.notificationStorage, + this.identifierStorage, + this.operationPendingStorage ); } return this.signifyNotificationService; diff --git a/src/core/agent/records/index.ts b/src/core/agent/records/index.ts index 2a688ae2d..9ee7df3a7 100644 --- a/src/core/agent/records/index.ts +++ b/src/core/agent/records/index.ts @@ -12,3 +12,4 @@ export * from "./basicStorage"; export * from "./connectionStorage"; export * from "./connectionNoteStorage"; export * from "./notificationStorage"; +export * from "./operationPendingStorage"; diff --git a/src/core/agent/records/operationPendingRecord.test.ts b/src/core/agent/records/operationPendingRecord.test.ts new file mode 100644 index 000000000..a55f4778b --- /dev/null +++ b/src/core/agent/records/operationPendingRecord.test.ts @@ -0,0 +1,39 @@ +import { + OperationPendingRecord, + OperationPendingRecordStorageProps, +} from "./operationPendingRecord"; +import { OperationPendingRecordType } from "./operationPendingRecord.type"; + +const mockData: OperationPendingRecordStorageProps = { + id: "id", + recordType: OperationPendingRecordType.Witness, +}; + +describe("Operation pending record", () => { + test("should fill the record based on supplied props", () => { + const createdAt = new Date(); + const settingsRecord = new OperationPendingRecord({ + ...mockData, + createdAt: createdAt, + }); + settingsRecord.getTags(); + expect(settingsRecord.id).toBe(mockData.id); + expect(settingsRecord.createdAt).toBe(createdAt); + expect(settingsRecord.recordType).toBe(mockData.recordType); + expect(settingsRecord.getTags()).toMatchObject({ + recordType: mockData.recordType, + }); + }); + + test("should fallback to the current time if not supplied", async () => { + const createdAt = new Date(); + await new Promise((resolve) => setTimeout(resolve, 10)); + const settingsRecord = new OperationPendingRecord({ + id: mockData.id, + recordType: mockData.recordType, + }); + expect(settingsRecord.createdAt.getTime()).toBeGreaterThan( + createdAt.getTime() + ); + }); +}); diff --git a/src/core/agent/records/operationPendingRecord.ts b/src/core/agent/records/operationPendingRecord.ts new file mode 100644 index 000000000..b3cae3577 --- /dev/null +++ b/src/core/agent/records/operationPendingRecord.ts @@ -0,0 +1,36 @@ +import { v4 as uuidv4 } from "uuid"; +import { BaseRecord, Tags } from "../../storage/storage.types"; +import { OperationPendingRecordType } from "./operationPendingRecord.type"; + +interface OperationPendingRecordStorageProps { + id?: string; + createdAt?: Date; + tags?: Tags; + recordType: OperationPendingRecordType; +} + +class OperationPendingRecord extends BaseRecord { + recordType!: OperationPendingRecordType; + static readonly type = "OperationPendingRecord"; + readonly type = OperationPendingRecord.type; + + constructor(props: OperationPendingRecordStorageProps) { + super(); + if (props) { + this.id = props.id ?? uuidv4(); + this.createdAt = props.createdAt ?? new Date(); + this.recordType = props.recordType; + this._tags = props.tags ?? {}; + } + } + + getTags() { + return { + ...this._tags, + recordType: this.recordType, + }; + } +} + +export type { OperationPendingRecordStorageProps }; +export { OperationPendingRecord }; diff --git a/src/core/agent/records/operationPendingRecord.type.ts b/src/core/agent/records/operationPendingRecord.type.ts new file mode 100644 index 000000000..4cdaa442c --- /dev/null +++ b/src/core/agent/records/operationPendingRecord.type.ts @@ -0,0 +1,3 @@ +export enum OperationPendingRecordType { + Witness = "witness", +} diff --git a/src/core/agent/records/operationPendingStorage.test.ts b/src/core/agent/records/operationPendingStorage.test.ts new file mode 100644 index 000000000..104f9766c --- /dev/null +++ b/src/core/agent/records/operationPendingStorage.test.ts @@ -0,0 +1,144 @@ +import { Query, StorageService } from "../../storage/storage.types"; +import { + OperationPendingRecord, + OperationPendingRecordStorageProps, +} from "./operationPendingRecord"; +import { OperationPendingStorage } from "./operationPendingStorage"; +import { OperationPendingRecordType } from "./operationPendingRecord.type"; + +const storageService = jest.mocked>({ + save: jest.fn(), + delete: jest.fn(), + deleteById: jest.fn(), + update: jest.fn(), + findById: jest.fn(), + findAllByQuery: jest.fn(), + getAll: jest.fn(), +}); + +const operationPendingStorage = new OperationPendingStorage(storageService); + +const id1 = "id1"; +const id2 = "id2"; + +const now = new Date(); + +const operationPendingRecordStorageProps: OperationPendingRecordStorageProps = { + id: id1, + createdAt: now, + recordType: OperationPendingRecordType.Witness, + tags: {}, +}; + +const operationPendingRecordA = new OperationPendingRecord( + operationPendingRecordStorageProps +); + +const operationPendingRecordB = new OperationPendingRecord({ + ...operationPendingRecordStorageProps, + id: id2, +}); + +describe("Operation Pending Storage", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test("Should save operation pending record", async () => { + storageService.save.mockResolvedValue(operationPendingRecordA); + await operationPendingStorage.save(operationPendingRecordStorageProps); + expect(storageService.save).toBeCalledWith(operationPendingRecordA); + }); + + test("Should delete operation pending record", async () => { + storageService.delete.mockResolvedValue(); + await operationPendingStorage.delete(operationPendingRecordA); + expect(storageService.delete).toBeCalledWith(operationPendingRecordA); + }); + + test("Should delete operation pending record by ID", async () => { + storageService.deleteById.mockResolvedValue(); + await operationPendingStorage.deleteById(operationPendingRecordA.id); + expect(storageService.deleteById).toBeCalledWith( + operationPendingRecordA.id + ); + }); + + test("Should update operation pending record", async () => { + storageService.update.mockResolvedValue(); + await operationPendingStorage.update(operationPendingRecordA); + expect(storageService.update).toBeCalledWith(operationPendingRecordA); + }); + + test("Should find operation pending record by ID", async () => { + storageService.findById.mockResolvedValue(operationPendingRecordA); + const result = await operationPendingStorage.findById( + operationPendingRecordA.id + ); + expect(result).toEqual(operationPendingRecordA); + }); + + test("Should find all operation pending records by query", async () => { + const query: Query = { + recordId: "1", + }; + const records = [operationPendingRecordA, operationPendingRecordB]; + storageService.findAllByQuery.mockResolvedValue(records); + const result = await operationPendingStorage.findAllByQuery(query); + expect(result).toEqual(records); + }); + + test("Should get all operation pending records", async () => { + const records = [operationPendingRecordA, operationPendingRecordB]; + storageService.getAll.mockResolvedValue(records); + const result = await operationPendingStorage.getAll(); + expect(result).toEqual(records); + }); + + // tests error + test("Should handle saving error", async () => { + storageService.save.mockRejectedValue(new Error("Saving error")); + await expect( + operationPendingStorage.save(operationPendingRecordStorageProps) + ).rejects.toThrow("Saving error"); + }); + + test("Should handle deleting error", async () => { + storageService.delete.mockRejectedValue(new Error("Deleting error")); + await expect( + operationPendingStorage.delete(operationPendingRecordA) + ).rejects.toThrow("Deleting error"); + }); + + test("Should handle updating error", async () => { + storageService.update.mockRejectedValue(new Error("Updating error")); + await expect( + operationPendingStorage.update(operationPendingRecordA) + ).rejects.toThrow("Updating error"); + }); + + test("Should handle finding error", async () => { + storageService.findById.mockRejectedValue(new Error("Finding error")); + await expect( + operationPendingStorage.findById(operationPendingRecordA.id) + ).rejects.toThrow("Finding error"); + }); + + test("Should handle not found", async () => { + storageService.findById.mockResolvedValue(null); + const result = await operationPendingStorage.findById("nonexistentId"); + expect(result).toBeNull(); + }); + + test("Should handle empty result", async () => { + storageService.findAllByQuery.mockResolvedValue([]); + const result = await operationPendingStorage.findAllByQuery({ filter: {} }); + expect(result).toEqual([]); + }); + + test("Should handle empty result for getAll", async () => { + storageService.getAll.mockResolvedValue([]); + const result = await operationPendingStorage.getAll(); + expect(result).toEqual([]); + }); +}); diff --git a/src/core/agent/records/operationPendingStorage.ts b/src/core/agent/records/operationPendingStorage.ts new file mode 100644 index 000000000..14937b137 --- /dev/null +++ b/src/core/agent/records/operationPendingStorage.ts @@ -0,0 +1,36 @@ +import { Query, StorageService } from "../../storage/storage.types"; +import { OperationPendingRecord, OperationPendingRecordStorageProps } from "./operationPendingRecord"; + + +class OperationPendingStorage { + private storageService: StorageService; + + constructor(storageService: StorageService) { + this.storageService = storageService; + } + + save(props: OperationPendingRecordStorageProps): Promise { + const record = new OperationPendingRecord(props); + return this.storageService.save(record); + } + delete(record: OperationPendingRecord): Promise { + return this.storageService.delete(record); + } + deleteById(id: string): Promise { + return this.storageService.deleteById(id); + } + update(record: OperationPendingRecord): Promise { + return this.storageService.update(record); + } + findById(id: string): Promise { + return this.storageService.findById(id, OperationPendingRecord); + } + findAllByQuery(query: Query): Promise { + return this.storageService.findAllByQuery(query, OperationPendingRecord); + } + getAll(): Promise { + return this.storageService.getAll(OperationPendingRecord); + } +} + +export { OperationPendingStorage }; diff --git a/src/core/agent/services/identifier.types.ts b/src/core/agent/services/identifier.types.ts index 2a30302a2..526a4d075 100644 --- a/src/core/agent/services/identifier.types.ts +++ b/src/core/agent/services/identifier.types.ts @@ -45,6 +45,7 @@ interface MultiSigIcpRequestDetails { interface CreateIdentifierResult { identifier: string; signifyName: string; + isPending: boolean; } export type { diff --git a/src/core/agent/services/identifierService.test.ts b/src/core/agent/services/identifierService.test.ts index 01bf40a5e..2bab5b507 100644 --- a/src/core/agent/services/identifierService.test.ts +++ b/src/core/agent/services/identifierService.test.ts @@ -8,6 +8,14 @@ const identifiersListMock = jest.fn(); const identifiersGetMock = jest.fn(); const identifiersCreateMock = jest.fn(); const identifiersRotateMock = jest.fn(); +const operationGetMock = jest.fn().mockImplementation((id: string) => { + return { + done: true, + response: { + i: id, + }, + }; +}); const signifyClient = jest.mocked({ connect: jest.fn(), @@ -22,14 +30,7 @@ const signifyClient = jest.mocked({ members: jest.fn(), }), operations: () => ({ - get: jest.fn().mockImplementation((id: string) => { - return { - done: true, - response: { - i: id, - }, - }; - }), + get: operationGetMock, }), oobis: () => ({ get: jest.fn(), @@ -85,6 +86,10 @@ const identifierStorage = jest.mocked({ getIdentifierMetadataByGroupId: jest.fn(), }); +const operationPendingStorage = jest.mocked({ + save: jest.fn(), +}); + const agentServicesProps = { signifyClient: signifyClient as any, eventService: new EventService(), @@ -92,7 +97,8 @@ const agentServicesProps = { const identifierService = new IdentifierService( agentServicesProps, - identifierStorage as any + identifierStorage as any, + operationPendingStorage as any ); jest.mock("../../../core/agent/agent", () => ({ @@ -102,6 +108,9 @@ jest.mock("../../../core/agent/agent", () => ({ getConnectionShortDetailById: jest.fn(), getConnections: jest.fn(), }, + signifyNotifications: { + addPendingOperationToQueue: jest.fn(), + }, getKeriaOnlineStatus: jest.fn(), }, }, @@ -230,16 +239,61 @@ describe("Single sig service of agent", () => { i: aid, }, }, - op: jest.fn(), + op: jest.fn().mockResolvedValue({ + name: "op123", + done: true, + }), }); expect( await identifierService.createIdentifier({ displayName, theme: 0, }) - ).toEqual({ identifier: aid, signifyName: expect.any(String) }); + ).toEqual({ + identifier: aid, + signifyName: expect.any(String), + isPending: false, + }); + expect(identifiersCreateMock).toBeCalled(); + expect(identifierStorage.createIdentifierMetadataRecord).toBeCalledTimes(1); + }); + + test("can create a keri identifier with pending operation", async () => { + Agent.agent.getKeriaOnlineStatus = jest.fn().mockReturnValueOnce(true); + const aid = "newIdentifierAid"; + const displayName = "newDisplayName"; + identifiersCreateMock.mockResolvedValue({ + serder: { + ked: { + i: aid, + }, + }, + op: jest.fn().mockResolvedValue({ + name: "op123", + done: false, + }), + }); + operationGetMock.mockImplementation((id: string) => { + return { + done: false, + response: { + i: id, + }, + }; + }); + expect( + await identifierService.createIdentifier({ + displayName, + theme: 0, + }) + ).toEqual({ + identifier: aid, + signifyName: expect.any(String), + isPending: true, + }); expect(identifiersCreateMock).toBeCalled(); expect(identifierStorage.createIdentifierMetadataRecord).toBeCalledTimes(1); + expect(operationPendingStorage.save).toBeCalledTimes(1); }); test("cannot create a keri identifier if theme is not valid", async () => { diff --git a/src/core/agent/services/identifierService.ts b/src/core/agent/services/identifierService.ts index 892d995e6..d3eb3339e 100644 --- a/src/core/agent/services/identifierService.ts +++ b/src/core/agent/services/identifierService.ts @@ -15,6 +15,9 @@ import { AgentServicesProps, IdentifierResult } from "../agent.types"; import { IdentifierStorage } from "../records"; import { ConfigurationService } from "../../configuration"; import { BackingMode } from "../../configuration/configurationService.types"; +import { OperationPendingStorage } from "../records/operationPendingStorage"; +import { OperationPendingRecordType } from "../records/operationPendingRecord.type"; +import { Agent } from "../agent"; import { PeerConnection } from "../../cardano/walletConnect/peerConnection"; const identifierTypeThemes = [0, 1]; @@ -32,13 +35,16 @@ class IdentifierService extends AgentService { "Failed to obtain key manager for given AID"; protected readonly identifierStorage: IdentifierStorage; + protected readonly operationPendingStorage: OperationPendingStorage; constructor( agentServiceProps: AgentServicesProps, - identifierStorage: IdentifierStorage + identifierStorage: IdentifierStorage, + operationPendingStorage: OperationPendingStorage ) { super(agentServiceProps); this.identifierStorage = identifierStorage; + this.operationPendingStorage = operationPendingStorage; } async getIdentifiers(getArchived = false): Promise { @@ -124,23 +130,44 @@ class IdentifierService extends AgentService { "id" | "createdAt" | "isArchived" | "signifyName" > ): Promise { + const startTime = Date.now(); this.validIdentifierMetadata(metadata); const signifyName = uuidv4(); const operation = await this.props.signifyClient .identifiers() .create(signifyName); //, this.getCreateAidOptions()); - await operation.op(); + let op = await operation.op(); + const signifyOpName = op.name; const addRoleOperation = await this.props.signifyClient .identifiers() .addEndRole(signifyName, "agent", this.props.signifyClient.agent!.pre); await addRoleOperation.op(); const identifier = operation.serder.ked.i; + const isPending = !op.done; + if (isPending) { + op = await waitAndGetDoneOp( + this.props.signifyClient, + op, + 2000 - (Date.now() - startTime) + ); + if (!op.done) { + const pendingOperation = await this.operationPendingStorage.save({ + id: op.name, + recordType: OperationPendingRecordType.Witness, + }); + Agent.agent.signifyNotifications.addPendingOperationToQueue( + pendingOperation + ); + } + } await this.identifierStorage.createIdentifierMetadataRecord({ id: identifier, ...metadata, + signifyOpName: signifyOpName, + isPending: !op.done, signifyName: signifyName, }); - return { identifier, signifyName }; + return { identifier, signifyName, isPending: !op.done }; } async archiveIdentifier(identifier: string): Promise { diff --git a/src/core/agent/services/signifyNotificationService.test.ts b/src/core/agent/services/signifyNotificationService.test.ts index fdab0e800..e7ddb456f 100644 --- a/src/core/agent/services/signifyNotificationService.test.ts +++ b/src/core/agent/services/signifyNotificationService.test.ts @@ -99,9 +99,14 @@ const notificationStorage = jest.mocked({ getAll: jest.fn(), }); +const identifierStorage = jest.mocked({}); +const operationPendingStorage = jest.mocked({}); + const signifyNotificationService = new SignifyNotificationService( agentServicesProps, - notificationStorage as any + notificationStorage as any, + identifierStorage as any, + operationPendingStorage as any, ); jest.mock("../../../core/agent/agent", () => ({ diff --git a/src/core/agent/services/signifyNotificationService.ts b/src/core/agent/services/signifyNotificationService.ts index 81cff2756..c5a1aab8c 100644 --- a/src/core/agent/services/signifyNotificationService.ts +++ b/src/core/agent/services/signifyNotificationService.ts @@ -7,21 +7,36 @@ import { NotificationRoute, } from "../agent.types"; import { Notification } from "./credentialService.types"; -import { BasicRecord, NotificationStorage } from "../records"; +import { + BasicRecord, + IdentifierStorage, + NotificationStorage, + OperationPendingStorage, +} from "../records"; import { Agent } from "../agent"; +import { OperationPendingRecordType } from "../records/operationPendingRecord.type"; +import { OperationPendingRecord } from "../records/operationPendingRecord"; class SignifyNotificationService extends AgentService { static readonly NOTIFICATION_NOT_FOUND = "Notification record not found"; static readonly POLL_KERIA_INTERVAL = 5000; protected readonly notificationStorage!: NotificationStorage; + protected readonly identifierStorage: IdentifierStorage; + protected readonly operationPendingStorage: OperationPendingStorage; + + protected pendingOperations: OperationPendingRecord[] = []; constructor( agentServiceProps: AgentServicesProps, - notificationStorage: NotificationStorage + notificationStorage: NotificationStorage, + identifierStorage: IdentifierStorage, + operationPendingStorage: OperationPendingStorage ) { super(agentServiceProps); this.notificationStorage = notificationStorage; + this.identifierStorage = identifierStorage; + this.operationPendingStorage = operationPendingStorage; } async onNotificationStateChanged( @@ -218,6 +233,84 @@ class SignifyNotificationService extends AgentService { }); return notificationRecord; } + + async onSignifyOperationStateChanged( + callback: ({ + oid, + opType, + }: { + oid: string; + opType: OperationPendingRecordType; + }) => void + ) { + this.pendingOperations = await this.operationPendingStorage.getAll(); + // eslint-disable-next-line no-constant-condition + while (true) { + if (!Agent.agent.getKeriaOnlineStatus()) { + await new Promise((rs) => + setTimeout(rs, SignifyNotificationService.POLL_KERIA_INTERVAL) + ); + continue; + } + + if (this.pendingOperations.length > 0) { + for (const pendingOperation of this.pendingOperations) { + let operation; + try { + operation = await this.props.signifyClient + .operations() + .get(pendingOperation.id); + } catch (error) { + // Possible that bootAndConnect is called from @OnlineOnly in between loops, + // so check if its gone down to avoid having 2 bootAndConnect loops + if (Agent.agent.getKeriaOnlineStatus()) { + // This will hang the loop until the connection is secured again + await Agent.agent.connect(); + } + } + + if (operation && operation.done) { + const recordId = pendingOperation.id.replace( + `${pendingOperation.recordType}.`, + "" + ); + switch (pendingOperation.recordType) { + case OperationPendingRecordType.Witness: { + await this.identifierStorage.updateIdentifierMetadata( + recordId, + { + isPending: false, + } + ); + callback({ + opType: pendingOperation.recordType, + oid: recordId, + }); + + break; + } + default: + break; + } + await this.operationPendingStorage.deleteById(pendingOperation.id); + this.pendingOperations.splice( + this.pendingOperations.indexOf(pendingOperation), + 1 + ); + } + } + } + await new Promise((rs) => { + setTimeout(() => { + rs(true); + }, 250); + }); + } + } + + addPendingOperationToQueue(pendingOperation: OperationPendingRecord) { + this.pendingOperations.push(pendingOperation); + } } export { SignifyNotificationService }; diff --git a/src/store/reducers/identifiersCache/identifiersCache.test.ts b/src/store/reducers/identifiersCache/identifiersCache.test.ts index 1ae4eb942..ccd62bb0b 100644 --- a/src/store/reducers/identifiersCache/identifiersCache.test.ts +++ b/src/store/reducers/identifiersCache/identifiersCache.test.ts @@ -9,6 +9,8 @@ import { getFavouritesIdentifiersCache, setMultiSigGroupCache, getMultiSigGroupCache, + updateOrAddIdentifiersCache, + updateIsPending, } from "./identifiersCache"; import { RootState } from "../../index"; import { IdentifierShortDetails } from "../../../core/agent/services/identifier.types"; @@ -105,6 +107,66 @@ describe("identifiersCacheSlice", () => { ); expect(newState.favourites).toEqual([]); }); + + it("should handle updateOrAddIdentifiersCache", () => { + const identifiers: IdentifierShortDetails[] = [ + { + id: "id-1", + displayName: "example-name", + createdAtUTC: "example-date", + theme: 0, + isPending: false, + signifyName: "Test", + }, + ]; + const currentState = identifiersCacheSlice.reducer( + initialState, + setIdentifiersCache(identifiers) + ); + const identifier: IdentifierShortDetails = { + id: "id-2", + displayName: "example-name", + createdAtUTC: "example-date", + theme: 0, + isPending: false, + signifyName: "Test", + }; + const newState = identifiersCacheSlice.reducer( + currentState, + updateOrAddIdentifiersCache(identifier) + ); + expect(newState.identifiers).toEqual([...identifiers, identifier]); + }); + + it("should handle updateIsPending", () => { + const identifiers: IdentifierShortDetails[] = [ + { + id: "id-1", + displayName: "example-name", + createdAtUTC: "example-date", + theme: 0, + isPending: true, + signifyName: "Test", + }, + ]; + const currentState = identifiersCacheSlice.reducer( + initialState, + setIdentifiersCache(identifiers) + ); + const identifier: IdentifierShortDetails = { + id: "id-1", + displayName: "example-name", + createdAtUTC: "example-date", + theme: 0, + isPending: false, + signifyName: "Test", + }; + const newState = identifiersCacheSlice.reducer( + currentState, + updateIsPending({ id: identifier.id, isPending: identifier.isPending }) + ); + expect(newState.identifiers).toEqual([identifier]); + }); }); describe("get identifier Cache", () => { diff --git a/src/store/reducers/identifiersCache/identifiersCache.ts b/src/store/reducers/identifiersCache/identifiersCache.ts index 674a6ed61..0d6ba89b6 100644 --- a/src/store/reducers/identifiersCache/identifiersCache.ts +++ b/src/store/reducers/identifiersCache/identifiersCache.ts @@ -22,6 +22,30 @@ const identifiersCacheSlice = createSlice({ ) => { state.identifiers = action.payload; }, + updateOrAddIdentifiersCache: ( + state, + action: PayloadAction + ) => { + const identifiers = state.identifiers.filter( + (aid) => aid.id !== action.payload.id + ); + state.identifiers = [...identifiers, action.payload]; + }, + updateIsPending: ( + state, + action: PayloadAction> + ) => { + const identifier = state.identifiers.find( + (aid) => aid.id === action.payload.id + ); + if (identifier) { + identifier.isPending = action.payload.isPending; + state.identifiers = [ + ...state.identifiers.filter((aid) => aid.id !== action.payload.id), + identifier, + ]; + } + }, setFavouritesIdentifiersCache: ( state, action: PayloadAction @@ -54,6 +78,8 @@ export { initialState, identifiersCacheSlice }; export const { setIdentifiersCache, setFavouritesIdentifiersCache, + updateOrAddIdentifiersCache, + updateIsPending, addFavouriteIdentifierCache, removeFavouriteIdentifierCache, setMultiSigGroupCache, diff --git a/src/ui/App.test.tsx b/src/ui/App.test.tsx index 5173bc572..88cd580fc 100644 --- a/src/ui/App.test.tsx +++ b/src/ui/App.test.tsx @@ -51,6 +51,7 @@ jest.mock("../core/agent/agent", () => ({ }, signifyNotifications: { onNotificationStateChanged: jest.fn(), + onSignifyOperationStateChanged: jest.fn(), }, onKeriaStatusStateChanged: jest.fn(), peerConnectionMetadataStorage: { diff --git a/src/ui/components/AppWrapper/AppWrapper.test.tsx b/src/ui/components/AppWrapper/AppWrapper.test.tsx index 6d3cd79de..720ed15e7 100644 --- a/src/ui/components/AppWrapper/AppWrapper.test.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.test.tsx @@ -1,14 +1,15 @@ import { render, waitFor } from "@testing-library/react"; import { Provider } from "react-redux"; import { - AppWrapper, acdcChangeHandler, + AppWrapper, connectionStateChangedHandler, keriaNotificationsChangeHandler, peerConnectRequestSignChangeHandler, peerConnectedChangeHandler, peerConnectionBrokenChangeHandler, peerDisconnectedChangeHandler, + signifyOperationStateChangeHandler, } from "./AppWrapper"; import { store } from "../../../store"; import { Agent } from "../../../core/agent/agent"; @@ -47,6 +48,12 @@ import { setConnectedWallet, setWalletConnectionsCache, } from "../../../store/reducers/walletConnectionsCache"; +import { IdentifierShortDetails } from "../../../core/agent/services/identifier.types"; +import { + updateIsPending, + updateOrAddIdentifiersCache, +} from "../../../store/reducers/identifiersCache"; +import { OperationPendingRecordType } from "../../../core/agent/records/operationPendingRecord.type"; jest.mock("../../../core/agent/agent", () => ({ Agent: { @@ -92,6 +99,7 @@ jest.mock("../../../core/agent/agent", () => ({ }, signifyNotifications: { onNotificationStateChanged: jest.fn(), + onSignifyOperationStateChanged: jest.fn(), }, getKeriaOnlineStatus: jest.fn(), onKeriaStatusStateChanged: jest.fn(), @@ -367,3 +375,26 @@ describe("AppWrapper handler", () => { }); }); }); +describe("Signify operation state changed handler", () => { + test("handles operation updated", async () => { + const aid = { + id: "id", + displayName: "string", + createdAtUTC: "string", + signifyName: "string", + theme: 0, + isPending: false, + delegated: {}, + } as IdentifierShortDetails; + await signifyOperationStateChangeHandler( + { opType: OperationPendingRecordType.Witness, oid: aid.id }, + dispatch + ); + expect(dispatch).toBeCalledWith( + updateIsPending({ id: aid.id, isPending: false }) + ); + expect(dispatch).toBeCalledWith( + setToastMsg(ToastMsgType.IDENTIFIER_UPDATED) + ); + }); +}); diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index 9810444c5..97f47d266 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -14,6 +14,8 @@ import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; import { setFavouritesIdentifiersCache, setIdentifiersCache, + updateIsPending, + updateOrAddIdentifiersCache, } from "../../../store/reducers/identifiersCache"; import { setCredsCache, @@ -56,6 +58,8 @@ import { MultiSigService } from "../../../core/agent/services/multiSigService"; import { setViewTypeCache } from "../../../store/reducers/identifierViewTypeCache"; import { CardListViewType } from "../SwitchCardView"; import { setEnableBiometryCache } from "../../../store/reducers/biometryCache"; +import { IdentifierShortDetails } from "../../../core/agent/services/identifier.types"; +import { OperationPendingRecordType } from "../../../core/agent/records/operationPendingRecord.type"; import { i18n } from "../../../i18n"; import { Alert } from "../Alert"; @@ -193,6 +197,18 @@ const peerConnectionBrokenChangeHandler = async ( dispatch(setToastMsg(ToastMsgType.DISCONNECT_WALLET_SUCCESS)); }; +const signifyOperationStateChangeHandler = async ( + { oid, opType }: { oid: string; opType: OperationPendingRecordType }, + dispatch: ReturnType +) => { + switch (opType) { + case OperationPendingRecordType.Witness: + dispatch(updateIsPending({ id: oid, isPending: false })); + dispatch(setToastMsg(ToastMsgType.IDENTIFIER_UPDATED)); + break; + } +}; + const AppWrapper = (props: { children: ReactNode }) => { const dispatch = useAppDispatch(); const authentication = useAppSelector(getAuthentication); @@ -404,6 +420,9 @@ const AppWrapper = (props: { children: ReactNode }) => { return peerConnectionBrokenChangeHandler(event, dispatch); } ); + Agent.agent.signifyNotifications.onSignifyOperationStateChanged((event) => { + return signifyOperationStateChangeHandler(event, dispatch); + }); dispatch(setInitialized(true)); }; @@ -436,4 +455,5 @@ export { peerDisconnectedChangeHandler, peerConnectRequestSignChangeHandler, peerConnectionBrokenChangeHandler, + signifyOperationStateChangeHandler, }; diff --git a/src/ui/components/CreateIdentifier/components/IdentifierStage0.tsx b/src/ui/components/CreateIdentifier/components/IdentifierStage0.tsx index 15ddb9d4a..1d88547e7 100644 --- a/src/ui/components/CreateIdentifier/components/IdentifierStage0.tsx +++ b/src/ui/components/CreateIdentifier/components/IdentifierStage0.tsx @@ -94,7 +94,7 @@ const IdentifierStage0 = ({ }; } metadata.groupMetadata = groupMetadata; - const { identifier, signifyName } = + const { identifier, signifyName, isPending } = await Agent.agent.identifiers.createIdentifier(metadata); if (identifier) { const newIdentifier: IdentifierShortDetails = { @@ -102,7 +102,7 @@ const IdentifierStage0 = ({ displayName: state.displayNameValue, createdAtUTC: new Date().toISOString(), theme: state.selectedTheme, - isPending: false, + isPending: isPending, signifyName, }; if (groupMetadata) { diff --git a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx index d02476c8f..e37172a81 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx @@ -30,6 +30,19 @@ const notificationStorage = jest.mocked({ getAll: jest.fn(), }); +const identifierStorage = jest.mocked({ + open: jest.fn(), + save: jest.fn(), + delete: jest.fn(), + deleteById: jest.fn(), + update: jest.fn(), + findById: jest.fn(), + findAllByQuery: jest.fn(), + getAll: jest.fn(), +}); + +const operationPendingStorage = jest.mocked({}); + const identifiersListMock = jest.fn(); const identifiersGetMock = jest.fn(); const identifiersCreateMock = jest.fn(); @@ -112,7 +125,9 @@ const agentServicesProps = { const signifyNotificationService = new SignifyNotificationService( agentServicesProps, - notificationStorage as any + notificationStorage as any, + identifierStorage as any, + operationPendingStorage as any, ); describe("Multi-Sig request", () => { From 8768b9d136437f5e081f95c282128db5d4dfb01d Mon Sep 17 00:00:00 2001 From: Sotatek-DukeVu <162310763+Sotatek-DukeVu@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:22:09 +0700 Subject: [PATCH 07/28] feat(ui): Overlay IncomingRequest over the entire app (#503) * feat(ui): update side page using modal * feat(ui): update unit test * feat(ui): override android animation opacity * feat(ui): prevent render incomming request when user not logged in --------- Co-authored-by: Vu Van Duc --- .../components/SideSlider/SideSlider.test.tsx | 99 +++++++++++++++++++ src/ui/components/SideSlider/SideSlider.tsx | 49 ++++++++- .../components/SideSlider/SideSlider.types.ts | 5 +- src/ui/pages/SidePage/SidePage.test.tsx | 17 ++-- src/ui/pages/SidePage/SidePage.tsx | 14 ++- 5 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 src/ui/components/SideSlider/SideSlider.test.tsx diff --git a/src/ui/components/SideSlider/SideSlider.test.tsx b/src/ui/components/SideSlider/SideSlider.test.tsx new file mode 100644 index 000000000..d4805890c --- /dev/null +++ b/src/ui/components/SideSlider/SideSlider.test.tsx @@ -0,0 +1,99 @@ +import { fireEvent, render, waitFor } from "@testing-library/react"; +import { waitForIonicReact } from "@ionic/react-test-utils"; +import { act } from "react-dom/test-utils"; +import { SideSlider } from "./SideSlider"; + +describe("Side Slider", () => { + test("Render as modal", async () => { + const closeAnimation = jest.fn(); + const openAnimation = jest.fn(); + + const { getByText, getByTestId, rerender } = render( + +
Content
+
+ ); + + await waitForIonicReact(); + await waitFor(() => { + expect(getByText("Content")).toBeInTheDocument(); + + expect( + getByTestId("side-slider").classList.contains("side-slider-modal") + ).toBe(true); + }); + + await waitFor(() => { + expect(openAnimation).toBeCalled(); + }); + + rerender( + +
Content
+
+ ); + + await waitFor(() => { + expect(closeAnimation).toBeCalled(); + }); + }); + + test("Render as normal page", async () => { + const endAnimation = jest.fn(); + const openAnimation = jest.fn(); + + const { getByText, getByTestId, rerender } = render( + +
Content
+
+ ); + + await waitFor(() => { + expect(getByText("Content")).toBeInTheDocument(); + + expect( + getByTestId("side-slider").classList.contains("side-slider-container") + ).toBe(true); + }); + + act(() => { + fireEvent.transitionEnd(getByTestId("side-slider")); + }); + + await waitFor(() => { + expect(openAnimation).toBeCalled(); + }); + + rerender( + +
Content
+
+ ); + + act(() => { + fireEvent.transitionEnd(getByTestId("side-slider")); + }); + + await waitFor(() => { + expect(endAnimation).toBeCalled(); + }); + }); +}); diff --git a/src/ui/components/SideSlider/SideSlider.tsx b/src/ui/components/SideSlider/SideSlider.tsx index 330bbd9c4..35c59c8b0 100644 --- a/src/ui/components/SideSlider/SideSlider.tsx +++ b/src/ui/components/SideSlider/SideSlider.tsx @@ -1,3 +1,4 @@ +import { IonModal, createAnimation } from "@ionic/react"; import { SideSliderProps } from "./SideSlider.types"; import { combineClassNames } from "../../utils/style"; import "./SideSlider.scss"; @@ -7,10 +8,54 @@ const SIDE_SLIDER_Z_INDEX = 103; const SideSlider = ({ open, children, + renderAsModal = false, zIndex = SIDE_SLIDER_Z_INDEX, onOpenAnimationEnd, onCloseAnimationEnd, }: SideSliderProps) => { + if (renderAsModal) { + const slideAnimation = (baseEl: HTMLElement) => { + const root = baseEl.shadowRoot; + const modalWrapper = root?.querySelector(".modal-wrapper") ?? baseEl; + + return createAnimation() + .addElement(modalWrapper) + .easing("ease-out") + .duration(500) + .fromTo("transform", "translateX(100%)", "translateX(0)") + .fromTo("opacity", 1, 1) + .afterStyles({ + opacity: 1, + }); + }; + + const enterAnimation = (baseEl: HTMLElement) => { + return slideAnimation(baseEl).onFinish(() => { + onOpenAnimationEnd?.(); + }); + }; + + const leaveAnimation = (baseEl: HTMLElement) => { + return slideAnimation(baseEl) + .direction("reverse") + .onFinish((e) => { + onCloseAnimationEnd?.(); + }); + }; + + return ( + + {children} + + ); + } + const classes = combineClassNames( "side-slider-container", open ? "open" : "close" @@ -22,8 +67,8 @@ const SideSlider = ({ zIndex, }} data-testid="side-slider" - onTransitionEnd={(e) => { - open ? onOpenAnimationEnd?.(e) : onCloseAnimationEnd?.(e); + onTransitionEnd={() => { + open ? onOpenAnimationEnd?.() : onCloseAnimationEnd?.(); }} className={classes} > diff --git a/src/ui/components/SideSlider/SideSlider.types.ts b/src/ui/components/SideSlider/SideSlider.types.ts index babef9ac1..b6483ce68 100644 --- a/src/ui/components/SideSlider/SideSlider.types.ts +++ b/src/ui/components/SideSlider/SideSlider.types.ts @@ -5,8 +5,9 @@ interface SideSliderProps { children: ReactNode; duration?: number; zIndex?: number; - onOpenAnimationEnd?: (event: TransitionEvent) => void; - onCloseAnimationEnd?: (event: TransitionEvent) => void; + renderAsModal?: boolean; + onOpenAnimationEnd?: () => void; + onCloseAnimationEnd?: () => void; } export type { SideSliderProps }; diff --git a/src/ui/pages/SidePage/SidePage.test.tsx b/src/ui/pages/SidePage/SidePage.test.tsx index 4ba791932..77af8f8ae 100644 --- a/src/ui/pages/SidePage/SidePage.test.tsx +++ b/src/ui/pages/SidePage/SidePage.test.tsx @@ -3,7 +3,6 @@ import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; import { act } from "react-dom/test-utils"; import EN_TRANSLATIONS from "../../../locales/en/en.json"; -import { store } from "../../../store"; import { SidePage } from "./SidePage"; import { TabsRoutePath } from "../../../routes/paths"; import { walletConnectionsFix } from "../../__fixtures__/walletConnectionsFix"; @@ -12,6 +11,13 @@ import { setPauseQueueIncomingRequest } from "../../../store/reducers/stateCache import { IncomingRequestType } from "../../../store/reducers/stateCache/stateCache.types"; import { NotificationRoute } from "../../../core/agent/agent.types"; +jest.mock("@ionic/react", () => ({ + ...jest.requireActual("@ionic/react"), + IonModal: ({ children, ...props }: { children: any }) => ( +
{children}
+ ), +})); + describe("Side Page: wallet connect", () => { const initialStateFull = { stateCache: { @@ -68,15 +74,6 @@ describe("Side Page: wallet connect", () => { await waitFor(() => { expect(getByTestId("alert-decline-connect-confirm-button")).toBeVisible(); }); - - act(() => { - fireEvent.click(getByTestId("alert-decline-connect-confirm-button")); - fireEvent.transitionEnd(getByTestId("side-slider")); - }); - - await waitFor(() => { - expect(dispatchMock).toBeCalledWith(setPauseQueueIncomingRequest(false)); - }); }); }); diff --git a/src/ui/pages/SidePage/SidePage.tsx b/src/ui/pages/SidePage/SidePage.tsx index e53a4988f..6c4b9c1d0 100644 --- a/src/ui/pages/SidePage/SidePage.tsx +++ b/src/ui/pages/SidePage/SidePage.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { SideSlider } from "../../components/SideSlider"; import { getQueueIncomingRequest, + getStateCache, setPauseQueueIncomingRequest, } from "../../../store/reducers/stateCache"; import { useAppDispatch, useAppSelector } from "../../../store/hooks"; @@ -16,13 +17,14 @@ const SidePage = () => { const queueIncomingRequest = useAppSelector(getQueueIncomingRequest); const pendingDAppMeerkat = useAppSelector(getPendingDAppMeerkat); + const stateCache = useAppSelector(getStateCache); const canOpenIncomingRequest = queueIncomingRequest.queues.length > 0 && !queueIncomingRequest.isPaused; const canOpenPendingWalletConnection = !!pendingDAppMeerkat; useEffect(() => { - if (canOpenIncomingRequest) return; + if (canOpenIncomingRequest || !stateCache.authentication.loggedIn) return; if (canOpenPendingWalletConnection && !queueIncomingRequest.isPaused) { dispatch(setPauseQueueIncomingRequest(true)); @@ -30,6 +32,15 @@ const SidePage = () => { } }, [canOpenIncomingRequest, canOpenPendingWalletConnection]); + useEffect(() => { + if (!stateCache.authentication.loggedIn) return; + setOpenSidePage(canOpenIncomingRequest || canOpenPendingWalletConnection); + }, [ + canOpenIncomingRequest, + canOpenPendingWalletConnection, + stateCache.authentication.loggedIn, + ]); + const unpauseIncomingRequest = () => { if (pauseIncommingRequestByConnection.current) { dispatch(setPauseQueueIncomingRequest(false)); @@ -57,6 +68,7 @@ const SidePage = () => { return ( From 57dc94cc24ae90c3d09d1744c45494a4426282bb Mon Sep 17 00:00:00 2001 From: Sotatek-DukeVu <162310763+Sotatek-DukeVu@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:37:17 +0700 Subject: [PATCH 08/28] feat(ui): app spinner when sign transaction (#505) Co-authored-by: Vu Van Duc --- src/ui/components/Spinner/Spinner.scss | 14 +++ src/ui/components/Spinner/Spinner.tsx | 23 ++++ src/ui/components/Spinner/Spinner.type.ts | 12 ++ src/ui/components/Spinner/index.ts | 1 + .../components/SignRequest.tsx | 107 ++++++++++-------- 5 files changed, 107 insertions(+), 50 deletions(-) create mode 100644 src/ui/components/Spinner/Spinner.scss create mode 100644 src/ui/components/Spinner/Spinner.tsx create mode 100644 src/ui/components/Spinner/Spinner.type.ts create mode 100644 src/ui/components/Spinner/index.ts diff --git a/src/ui/components/Spinner/Spinner.scss b/src/ui/components/Spinner/Spinner.scss new file mode 100644 index 000000000..1f78aca4e --- /dev/null +++ b/src/ui/components/Spinner/Spinner.scss @@ -0,0 +1,14 @@ +.spinner-container { + display: flex; + height: 100%; + width: 100%; + align-items: center; + justify-content: center; + background-color: rgba(3, 3, 33, 0.2); + + &.screen { + position: absolute; + top: 0; + left: 0; + } +} diff --git a/src/ui/components/Spinner/Spinner.tsx b/src/ui/components/Spinner/Spinner.tsx new file mode 100644 index 000000000..0b2d65550 --- /dev/null +++ b/src/ui/components/Spinner/Spinner.tsx @@ -0,0 +1,23 @@ +import { IonSpinner } from "@ionic/react"; +import "./Spinner.scss"; +import { SpinnerConverage, SpinnerProps } from "./Spinner.type"; +import { combineClassNames } from "../../utils/style"; + +export const Spinner = ({ + show, + coverage = SpinnerConverage.Screen, +}: SpinnerProps) => { + if (!show) { + return null; + } + + const classes = combineClassNames("spinner-container", { + screen: coverage === SpinnerConverage.Screen, + }); + + return ( +
+ +
+ ); +}; diff --git a/src/ui/components/Spinner/Spinner.type.ts b/src/ui/components/Spinner/Spinner.type.ts new file mode 100644 index 000000000..4560a0cee --- /dev/null +++ b/src/ui/components/Spinner/Spinner.type.ts @@ -0,0 +1,12 @@ +enum SpinnerConverage { + Screen = "screen", + Container = "container", +} + +interface SpinnerProps { + show: boolean; + coverage?: SpinnerConverage; +} + +export type { SpinnerProps }; +export { SpinnerConverage }; diff --git a/src/ui/components/Spinner/index.ts b/src/ui/components/Spinner/index.ts new file mode 100644 index 000000000..2f83b9698 --- /dev/null +++ b/src/ui/components/Spinner/index.ts @@ -0,0 +1 @@ +export * from "./Spinner"; diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx index 5e46f9ac7..d30c5d113 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx @@ -10,11 +10,13 @@ import { ScrollablePageLayout } from "../../../../../components/layout/Scrollabl import CardanoLogo from "../../../../../assets/images/CardanoLogo.jpg"; import { RequestProps } from "../IncomingRequest.types"; import "./SignRequest.scss"; +import { Spinner } from "../../../../../components/Spinner"; const SignRequest = ({ pageId, activeStatus, requestData, + initiateAnimation, handleAccept, handleCancel, }: RequestProps) => { @@ -40,57 +42,62 @@ const SignRequest = ({ }; return ( - {`${i18n.t("request.sign.title")}`}} - > -
- {requestData.peerConnection?.name} + {`${i18n.t("request.sign.title")}`}} + > +
+ {requestData.peerConnection?.name} +

{requestData.peerConnection?.name}

+

{requestData.peerConnection?.url}

+
+
+ + + {signRequest?.payload.identifier} + + + + {isSigningObject ? ( + + ) : ( + + {signDetails.toString()} + + )} + +
+ -

{requestData.peerConnection?.name}

-

{requestData.peerConnection?.url}

-
-
- - - {signRequest?.payload.identifier} - - - - {isSigningObject ? ( - - ) : ( - {signDetails.toString()} - )} - -
- -
+ + + ); }; From 0e583b864dee83c904556616265170d5f392a2ef Mon Sep 17 00:00:00 2001 From: Bao Hoang <142210850+bao-sotatek@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:46:41 +0700 Subject: [PATCH 09/28] feat: move getting archived cred to redux (#504) * feat: move getting archived cred to redux * chore: modified state name * chore: remove unnecessary function --- .../agent/records/operationPendingStorage.ts | 18 ++++-- .../signifyNotificationService.test.ts | 2 +- src/routes/backRoute/backRoute.test.ts | 2 + src/routes/nextRoute/nextRoute.test.ts | 2 + src/store/index.ts | 2 + .../credsArchivedCache.test.ts | 62 +++++++++++++++++++ .../credsArchivedCache/credsArchivedCache.ts | 30 +++++++++ .../reducers/credsArchivedCache/index.ts | 1 + src/ui/components/AppWrapper/AppWrapper.tsx | 9 ++- .../ArchivedCredentials.test.tsx | 5 +- .../VerifyPasscode/VerifyPasscode.test.tsx | 1 + .../VerifyPassword/VerifyPassword.test.tsx | 2 + .../ConnectionDetails.test.tsx | 3 + .../CredentialDetails.test.tsx | 10 +++ .../CredentialDetails/CredentialDetails.tsx | 3 + src/ui/pages/Credentials/Credentials.test.tsx | 6 ++ src/ui/pages/Credentials/Credentials.tsx | 12 ++-- .../IncomingRequest/IncomingRequest.test.tsx | 2 +- 18 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 src/store/reducers/credsArchivedCache/credsArchivedCache.test.ts create mode 100644 src/store/reducers/credsArchivedCache/credsArchivedCache.ts create mode 100644 src/store/reducers/credsArchivedCache/index.ts diff --git a/src/core/agent/records/operationPendingStorage.ts b/src/core/agent/records/operationPendingStorage.ts index 14937b137..b11b62708 100644 --- a/src/core/agent/records/operationPendingStorage.ts +++ b/src/core/agent/records/operationPendingStorage.ts @@ -1,15 +1,19 @@ import { Query, StorageService } from "../../storage/storage.types"; -import { OperationPendingRecord, OperationPendingRecordStorageProps } from "./operationPendingRecord"; - +import { + OperationPendingRecord, + OperationPendingRecordStorageProps, +} from "./operationPendingRecord"; class OperationPendingStorage { private storageService: StorageService; - + constructor(storageService: StorageService) { this.storageService = storageService; } - - save(props: OperationPendingRecordStorageProps): Promise { + + save( + props: OperationPendingRecordStorageProps + ): Promise { const record = new OperationPendingRecord(props); return this.storageService.save(record); } @@ -25,7 +29,9 @@ class OperationPendingStorage { findById(id: string): Promise { return this.storageService.findById(id, OperationPendingRecord); } - findAllByQuery(query: Query): Promise { + findAllByQuery( + query: Query + ): Promise { return this.storageService.findAllByQuery(query, OperationPendingRecord); } getAll(): Promise { diff --git a/src/core/agent/services/signifyNotificationService.test.ts b/src/core/agent/services/signifyNotificationService.test.ts index e7ddb456f..a89e7033a 100644 --- a/src/core/agent/services/signifyNotificationService.test.ts +++ b/src/core/agent/services/signifyNotificationService.test.ts @@ -106,7 +106,7 @@ const signifyNotificationService = new SignifyNotificationService( agentServicesProps, notificationStorage as any, identifierStorage as any, - operationPendingStorage as any, + operationPendingStorage as any ); jest.mock("../../../core/agent/agent", () => ({ diff --git a/src/routes/backRoute/backRoute.test.ts b/src/routes/backRoute/backRoute.test.ts index 6157111bd..59f65fe7e 100644 --- a/src/routes/backRoute/backRoute.test.ts +++ b/src/routes/backRoute/backRoute.test.ts @@ -55,6 +55,7 @@ describe("getBackRoute", () => { }, }, credsCache: { creds: [], favourites: [] }, + credsArchivedCache: { creds: [] }, connectionsCache: { connections: [], }, @@ -181,6 +182,7 @@ describe("getPreviousRoute", () => { }, }, credsCache: { creds: [], favourites: [] }, + credsArchivedCache: { creds: [] }, connectionsCache: { connections: [], }, diff --git a/src/routes/nextRoute/nextRoute.test.ts b/src/routes/nextRoute/nextRoute.test.ts index 53991273a..0ed8e742a 100644 --- a/src/routes/nextRoute/nextRoute.test.ts +++ b/src/routes/nextRoute/nextRoute.test.ts @@ -54,6 +54,7 @@ describe("NextRoute", () => { }, }, credsCache: { creds: [], favourites: [] }, + credsArchivedCache: { creds: [] }, connectionsCache: { connections: [], }, @@ -186,6 +187,7 @@ describe("getNextRoute", () => { }, }, credsCache: { creds: [], favourites: [] }, + credsArchivedCache: { creds: [] }, connectionsCache: { connections: [], }, diff --git a/src/store/index.ts b/src/store/index.ts index 5f05b6b1f..5af20dd5c 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -7,6 +7,7 @@ import { connectionsCacheSlice } from "./reducers/connectionsCache"; import { walletConnectionsCacheSlice } from "./reducers/walletConnectionsCache"; import { identifierViewTypeCacheSlice } from "./reducers/identifierViewTypeCache"; import { biometryCacheSlice } from "./reducers/biometryCache"; +import { credsArchivedCacheSlice } from "./reducers/credsArchivedCache"; import { ssiAgentSlice } from "./reducers/ssiAgent"; const store = configureStore({ @@ -15,6 +16,7 @@ const store = configureStore({ seedPhraseCache: seedPhraseCacheSlice.reducer, identifiersCache: identifiersCacheSlice.reducer, credsCache: credsCacheSlice.reducer, + credsArchivedCache: credsArchivedCacheSlice.reducer, connectionsCache: connectionsCacheSlice.reducer, walletConnectionsCache: walletConnectionsCacheSlice.reducer, identifierViewTypeCacheCache: identifierViewTypeCacheSlice.reducer, diff --git a/src/store/reducers/credsArchivedCache/credsArchivedCache.test.ts b/src/store/reducers/credsArchivedCache/credsArchivedCache.test.ts new file mode 100644 index 000000000..e60d01402 --- /dev/null +++ b/src/store/reducers/credsArchivedCache/credsArchivedCache.test.ts @@ -0,0 +1,62 @@ +import { PayloadAction } from "@reduxjs/toolkit"; +import { + credsArchivedCacheSlice, + setCredsArchivedCache, + getCredsArchivedCache, +} from "./credsArchivedCache"; +import { RootState } from "../../index"; +import { CredentialMetadataRecordStatus } from "../../../core/agent/records/credentialMetadataRecord.types"; +import { CredentialShortDetails } from "../../../core/agent/services/credentialService.types"; + +describe("credsArchivedCacheSlice", () => { + const initialState = { + creds: [], + }; + it("should return the initial state", () => { + expect( + credsArchivedCacheSlice.reducer(undefined, {} as PayloadAction) + ).toEqual(initialState); + }); + + it("should handle setCredsArchivedCache", () => { + const creds: CredentialShortDetails[] = [ + { + id: "did:example:ebfeb1f712ebc6f1c276e12ec21", + issuanceDate: "2010-01-01T19:23:24Z", + credentialType: "University Credential", + status: CredentialMetadataRecordStatus.CONFIRMED, + }, + ]; + const newState = credsArchivedCacheSlice.reducer( + initialState, + setCredsArchivedCache(creds) + ); + expect(newState.creds).toEqual(creds); + }); +}); + +describe("get methods for CredsArchivedCache", () => { + it("should return the creds archived cache from RootState", () => { + const state = { + credsArchivedCache: { + creds: [ + { + id: "did:example:ebfeb1f712ebc6f1c276e12ec21", + issuanceDate: "2010-01-01T19:23:24Z", + credentialType: "University Credential", + nameOnCredential: "Thomas A. Mayfield", + status: "confirmed", + }, + { + id: "did:example:ebfeb1f712ebc6f1c276e12ec22", + issuanceDate: "2010-01-01T19:23:24Z", + credentialType: "University Credential", + status: "confirmed", + }, + ], + }, + } as RootState; + const credsArchivedCache = getCredsArchivedCache(state); + expect(credsArchivedCache).toEqual(state.credsArchivedCache.creds); + }); +}); diff --git a/src/store/reducers/credsArchivedCache/credsArchivedCache.ts b/src/store/reducers/credsArchivedCache/credsArchivedCache.ts new file mode 100644 index 000000000..12cc75e56 --- /dev/null +++ b/src/store/reducers/credsArchivedCache/credsArchivedCache.ts @@ -0,0 +1,30 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { RootState } from "../../index"; +import { CredentialShortDetails } from "../../../core/agent/services/credentialService.types"; + +const initialState: { + creds: CredentialShortDetails[]; +} = { + creds: [], +}; +const credsArchivedCacheSlice = createSlice({ + name: "credsArchivedCache", + initialState, + reducers: { + setCredsArchivedCache: ( + state, + action: PayloadAction + ) => { + state.creds = action.payload; + }, + }, +}); + +export { initialState, credsArchivedCacheSlice }; + +export const { setCredsArchivedCache } = credsArchivedCacheSlice.actions; + +const getCredsArchivedCache = (state: RootState) => + state.credsArchivedCache.creds; + +export { getCredsArchivedCache }; diff --git a/src/store/reducers/credsArchivedCache/index.ts b/src/store/reducers/credsArchivedCache/index.ts new file mode 100644 index 000000000..6c17f0c6e --- /dev/null +++ b/src/store/reducers/credsArchivedCache/index.ts @@ -0,0 +1 @@ +export * from "./credsArchivedCache"; diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index 97f47d266..b19eba019 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -58,6 +58,7 @@ import { MultiSigService } from "../../../core/agent/services/multiSigService"; import { setViewTypeCache } from "../../../store/reducers/identifierViewTypeCache"; import { CardListViewType } from "../SwitchCardView"; import { setEnableBiometryCache } from "../../../store/reducers/biometryCache"; +import { setCredsArchivedCache } from "../../../store/reducers/credsArchivedCache"; import { IdentifierShortDetails } from "../../../core/agent/services/identifier.types"; import { OperationPendingRecordType } from "../../../core/agent/records/operationPendingRecord.type"; import { i18n } from "../../../i18n"; @@ -272,13 +273,17 @@ const AppWrapper = (props: { children: ReactNode }) => { const loadDatabase = async () => { const connectionsDetails = await Agent.agent.connections.getConnections(); - const credentials = await Agent.agent.credentials.getCredentials(); + const credsCache = await Agent.agent.credentials.getCredentials(); + const credsArchivedCache = await Agent.agent.credentials.getCredentials( + true + ); const storedIdentifiers = await Agent.agent.identifiers.getIdentifiers(); const storedPeerConnections = await Agent.agent.peerConnectionMetadataStorage.getAllPeerConnectionMetadata(); dispatch(setIdentifiersCache(storedIdentifiers)); - dispatch(setCredsCache(credentials)); + dispatch(setCredsCache(credsCache)); + dispatch(setCredsArchivedCache(credsArchivedCache)); dispatch(setConnectionsCache(connectionsDetails)); dispatch(setWalletConnectionsCache(storedPeerConnections)); }; diff --git a/src/ui/components/ArchivedCredentials/ArchivedCredentials.test.tsx b/src/ui/components/ArchivedCredentials/ArchivedCredentials.test.tsx index 756765c31..709394172 100644 --- a/src/ui/components/ArchivedCredentials/ArchivedCredentials.test.tsx +++ b/src/ui/components/ArchivedCredentials/ArchivedCredentials.test.tsx @@ -10,11 +10,12 @@ import EN_TRANSLATIONS from "../../../locales/en/en.json"; const deleteCredentailsMock = jest.fn((id: string) => Promise.resolve(true)); jest.mock("../../../core/agent/agent", () => ({ - AriesAgent: { + Agent: { agent: { credentials: { restoreCredential: jest.fn((id: string) => Promise.resolve(id)), deleteCredential: (id: string) => deleteCredentailsMock(id), + getCredentials: jest.fn().mockResolvedValue([]), }, }, }, @@ -135,7 +136,7 @@ describe("Creds Tab", () => { ); await waitFor(() => { - expect(dispatchMock).toBeCalledTimes(1); + expect(dispatchMock).toBeCalledTimes(2); }); }); diff --git a/src/ui/components/VerifyPasscode/VerifyPasscode.test.tsx b/src/ui/components/VerifyPasscode/VerifyPasscode.test.tsx index b3a8e0ff6..894568a62 100644 --- a/src/ui/components/VerifyPasscode/VerifyPasscode.test.tsx +++ b/src/ui/components/VerifyPasscode/VerifyPasscode.test.tsx @@ -48,6 +48,7 @@ const initialStateNoPassword = { bran: "bran", }, credsCache: { creds: credsFixAcdc }, + credsArchivedCache: { creds: credsFixAcdc }, biometryCache: { enabled: false, }, diff --git a/src/ui/components/VerifyPassword/VerifyPassword.test.tsx b/src/ui/components/VerifyPassword/VerifyPassword.test.tsx index 76a8a879c..f3ce17fc4 100644 --- a/src/ui/components/VerifyPassword/VerifyPassword.test.tsx +++ b/src/ui/components/VerifyPassword/VerifyPassword.test.tsx @@ -61,6 +61,7 @@ const initialStateNoPassword = { bran: "bran", }, credsCache: { creds: credsFixAcdc }, + credsArchivedCache: { creds: credsFixAcdc }, }; const initialStateWithPassword = { @@ -80,6 +81,7 @@ const initialStateWithPassword = { bran: "bran", }, credsCache: { creds: credsFixAcdc }, + credsArchivedCache: { creds: credsFixAcdc }, }; describe("Verify Password on Cards Details page", () => { diff --git a/src/ui/pages/ConnectionDetails/ConnectionDetails.test.tsx b/src/ui/pages/ConnectionDetails/ConnectionDetails.test.tsx index f6da71757..54e0de589 100644 --- a/src/ui/pages/ConnectionDetails/ConnectionDetails.test.tsx +++ b/src/ui/pages/ConnectionDetails/ConnectionDetails.test.tsx @@ -54,6 +54,9 @@ const initialStateFull = { credsCache: { creds: filteredCredsFix, }, + credsArchivedCache: { + creds: filteredCredsFix, + }, connectionsCache: { connections: connectionsFix, }, diff --git a/src/ui/pages/CredentialDetails/CredentialDetails.test.tsx b/src/ui/pages/CredentialDetails/CredentialDetails.test.tsx index a2798c23d..3a32e5886 100644 --- a/src/ui/pages/CredentialDetails/CredentialDetails.test.tsx +++ b/src/ui/pages/CredentialDetails/CredentialDetails.test.tsx @@ -9,6 +9,7 @@ import EN_TRANSLATIONS from "../../../locales/en/en.json"; import { addFavouritesCredsCache, removeFavouritesCredsCache, + setCredsCache, } from "../../../store/reducers/credsCache"; import { setCurrentRoute, @@ -18,6 +19,7 @@ import { credsFixAcdc } from "../../__fixtures__/credsFix"; import { TabsRoutePath } from "../../components/navigation/TabsMenu"; import { ToastMsgType } from "../../globals/types"; import { CredentialDetails } from "./CredentialDetails"; +import { setCredsArchivedCache } from "../../../store/reducers/credsArchivedCache"; const path = TabsRoutePath.CREDENTIALS + "/" + credsFixAcdc[0].id; @@ -28,6 +30,7 @@ jest.mock("../../../core/agent/agent", () => ({ getCredentialDetailsById: jest.fn(), restoreCredential: jest.fn(() => Promise.resolve(true)), getCredentialShortDetailsById: jest.fn(() => Promise.resolve([])), + getCredentials: jest.fn(() => Promise.resolve(true)), }, basicStorage: { findById: jest.fn(), @@ -91,6 +94,7 @@ const initialStateNoPasswordCurrent = { bran: "bran", }, credsCache: { creds: credsFixAcdc, favourites: [] }, + credsArchivedCache: { creds: credsFixAcdc }, biometryCache: { enabled: false, }, @@ -113,6 +117,7 @@ const initialStateNoPasswordArchived = { bran: "bran", }, credsCache: { creds: [] }, + credsArchivedCache: { creds: [] }, biometryCache: { enabled: false, }, @@ -176,6 +181,7 @@ describe("Cards Details page - current not archived credential", () => { bran: "bran", }, credsCache: { creds: credsFixAcdc, favourites: [] }, + credsArchivedCache: { creds: credsFixAcdc }, connectionsCache: { connections: [], }, @@ -581,6 +587,10 @@ describe("Cards Details page - archived credential", () => { path: TabsRoutePath.CREDENTIALS, }) ); + + credDispatchMock.mockImplementation((action) => { + expect(action).toEqual(setCredsCache(credsFixAcdc)); + }); }); }); }); diff --git a/src/ui/pages/CredentialDetails/CredentialDetails.tsx b/src/ui/pages/CredentialDetails/CredentialDetails.tsx index e6aac35fd..aa3585e11 100644 --- a/src/ui/pages/CredentialDetails/CredentialDetails.tsx +++ b/src/ui/pages/CredentialDetails/CredentialDetails.tsx @@ -46,6 +46,7 @@ import { combineClassNames } from "../../utils/style"; import { useAppIonRouter } from "../../hooks"; import { MiscRecordId } from "../../../core/agent/agent.types"; import { BasicRecord } from "../../../core/agent/records"; +import { setCredsArchivedCache } from "../../../store/reducers/credsArchivedCache"; const NAVIGATION_DELAY = 250; const CLEAR_ANIMATION = 1000; @@ -121,6 +122,7 @@ const CredentialDetails = () => { handleSetFavourite(params.id); } dispatch(setCredsCache(creds)); + dispatch(setToastMsg(ToastMsgType.CREDENTIAL_ARCHIVED)); }; @@ -137,6 +139,7 @@ const CredentialDetails = () => { params.id ); dispatch(setCredsCache([...credsCache, creds])); + dispatch(setToastMsg(ToastMsgType.CREDENTIAL_RESTORED)); handleDone(); }; diff --git a/src/ui/pages/Credentials/Credentials.test.tsx b/src/ui/pages/Credentials/Credentials.test.tsx index d1b84e7a8..836627e97 100644 --- a/src/ui/pages/Credentials/Credentials.test.tsx +++ b/src/ui/pages/Credentials/Credentials.test.tsx @@ -33,6 +33,9 @@ const initialStateEmpty = { credsCache: { creds: [], }, + credsArchivedCache: { + creds: filteredCredsFix, + }, connectionsCache: { connections: [], }, @@ -57,6 +60,9 @@ const initialStateFull = { }, ], }, + credsArchivedCache: { + creds: filteredCredsFix, + }, connectionsCache: { connections: connectionsFix, }, diff --git a/src/ui/pages/Credentials/Credentials.tsx b/src/ui/pages/Credentials/Credentials.tsx index ee72f7733..b330ea111 100644 --- a/src/ui/pages/Credentials/Credentials.tsx +++ b/src/ui/pages/Credentials/Credentials.tsx @@ -29,6 +29,10 @@ import { import { CredentialShortDetails } from "../../../core/agent/services/credentialService.types"; import { StartAnimationSource } from "../Identifiers/Identifiers.type"; import { useToggleConnections } from "../../hooks"; +import { + getCredsArchivedCache, + setCredsArchivedCache, +} from "../../../store/reducers/credsArchivedCache"; const CLEAR_STATE_DELAY = 1000; @@ -75,11 +79,10 @@ const Creds = () => { const pageId = "credentials-tab"; const dispatch = useAppDispatch(); const credsCache = useAppSelector(getCredsCache); + const archivedCreds = useAppSelector(getCredsArchivedCache); const favCredsCache = useAppSelector(getFavouritesCredsCache); const toastMsg = useAppSelector(getToastMsg); - const [archivedCreds, setArchivedCreds] = useState( - [] - ); + const [archivedCredentialsIsOpen, setArchivedCredentialsIsOpen] = useState(false); const [showPlaceholder, setShowPlaceholder] = useState(true); @@ -93,7 +96,7 @@ const Creds = () => { const fetchArchivedCreds = async () => { // @TODO - sdisalvo: handle error const creds = await Agent.agent.credentials.getCredentials(true); - setArchivedCreds(creds); + dispatch(setCredsArchivedCache(creds)); }; useEffect(() => { @@ -120,7 +123,6 @@ const Creds = () => { useIonViewWillEnter(() => { dispatch(setCurrentRoute({ path: TabsRoutePath.CREDENTIALS })); - fetchArchivedCreds(); }); const findTimeById = (id: string) => { diff --git a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx index e37172a81..699c1786a 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx @@ -127,7 +127,7 @@ const signifyNotificationService = new SignifyNotificationService( agentServicesProps, notificationStorage as any, identifierStorage as any, - operationPendingStorage as any, + operationPendingStorage as any ); describe("Multi-Sig request", () => { From 6e72a383451473d66eae44c92156246a7af8a95e Mon Sep 17 00:00:00 2001 From: Sotatek-DukeVu <162310763+Sotatek-DukeVu@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:10:16 +0700 Subject: [PATCH 10/28] feat(ui): Remove green tick animation when clicking Connect (#506) * feat(ui): remove unnecessary animation when connection wallet * feat(ui): remove checkmark icon --------- Co-authored-by: Vu Van Duc --- .../WalletConnect/WalletConnect.scss | 83 ++----------------- .../WalletConnect/WalletConnect.tsx | 41 +++++---- .../WalletConnect/WalletConnectStageOne.tsx | 16 +--- 3 files changed, 33 insertions(+), 107 deletions(-) diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.scss b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.scss index e761b0cb6..0ea81dd54 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.scss +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.scss @@ -1,51 +1,10 @@ -.connect-wallet-stage-one { - &.animation-on { - .request-animation-center { - .wallet-connect-message { - animation: scaleIcon 0.25s forwards; - animation-direction: reverse; - } - - .request-icons-row { - .request-user-logo { - animation: scaleIcon 0.25s forwards; - animation-direction: reverse; - } - - .request-checkmark-logo { - animation-delay: 0.25s; - animation: scaleIcon 0.5s forwards; - } - } - } - - .request-footer { - animation: fade 0.5s forwards; - } - - @keyframes scaleIcon { - 0% { - transform: scale(0); - opacity: 0; - } - - 100% { - transform: scale(1); - opacity: 1; - } - } - - @keyframes fade { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - } - } - } +.wallet-connect-container { + width: 100%; + height: 100%; + background: var(--ion-color-light); +} +.connect-wallet-stage-one { & .page-header { ion-toolbar { --background: transparent; @@ -80,41 +39,11 @@ } } - .request-checkmark-logo { - width: 4.875rem; - height: 4.875rem; - position: absolute; - transform: scale(0); - opacity: 0; - - span { - border-radius: 50%; - width: 100%; - height: 100%; - align-items: center; - display: flex; - flex-direction: column; - justify-content: center; - background: var(--ion-color-primary-gradient); - margin: 0 auto; - - & > ion-icon { - width: 50%; - height: 50%; - } - } - } - @media screen and (min-width: 250px) and (max-width: 370px) { .request-user-logo { width: 3.5rem; height: 3.5rem; } - - .request-checkmark-logo { - width: 3.5rem; - height: 3.5rem; - } } } diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx index af18746bb..4a0e848de 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx @@ -7,20 +7,28 @@ import { import { WalletConnectStageOne } from "./WalletConnectStageOne"; import { WalletConnectStageTwo } from "./WalletConnectStageTwo"; import { SidePageContentProps } from "../../SidePage.types"; +import { SideSlider } from "../../../../components/SideSlider"; const WalletConnect = ({ setOpenPage }: SidePageContentProps) => { const dispatch = useAppDispatch(); const pendingDAppMeerkat = useAppSelector(getPendingDAppMeerkat); const [requestStage, setRequestStage] = useState(0); + const [hiddenStageOne, setHiddenStageOne] = useState(false); useEffect(() => { setTimeout(() => setOpenPage(!!pendingDAppMeerkat), 10); }, [pendingDAppMeerkat]); const changeToStageTwo = () => { + setTimeout(() => setHiddenStageOne(true), 400); setRequestStage(1); }; + const backToStageOne = () => { + setHiddenStageOne(false); + setRequestStage(0); + }; + const handleCloseWalletConnect = () => { setOpenPage(false); @@ -31,23 +39,24 @@ const WalletConnect = ({ setOpenPage }: SidePageContentProps) => { if (!pendingDAppMeerkat) return null; - if (requestStage === 0) { - return ( - - ); - } - return ( - setRequestStage(0)} - /> +
+ {!hiddenStageOne && ( + + )} + + + +
); }; diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageOne.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageOne.tsx index 07b6994f5..797ed1880 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageOne.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageOne.tsx @@ -1,5 +1,5 @@ import { IonIcon } from "@ionic/react"; -import { checkmark, personCircleOutline } from "ionicons/icons"; +import { personCircleOutline } from "ionicons/icons"; import { useState } from "react"; import { i18n } from "../../../../../i18n"; import { Alert } from "../../../../components/Alert"; @@ -17,13 +17,10 @@ const WalletConnectStageOne = ({ onAccept, }: WalletConnectStageOneProps) => { const [openDeclineAlert, setOpenDeclineAlert] = useState(false); - const [acceptAnimation, setAcceptAnimation] = useState(false); const classes = combineClassNames(className, { show: !!isOpen, hide: !isOpen, - "animation-on": acceptAnimation, - "animation-off": !acceptAnimation, }); const openDecline = () => { @@ -35,11 +32,7 @@ const WalletConnectStageOne = ({ }; const handleAccept = () => { - setAcceptAnimation(true); - - setTimeout(() => { - onAccept(); - }, 700); + onAccept(); }; return ( @@ -69,11 +62,6 @@ const WalletConnectStageOne = ({ color="light" />
-
- - - -

Date: Thu, 6 Jun 2024 17:52:40 +0700 Subject: [PATCH 11/28] feat(ui): remove last separator of connection list (#507) * feat(ui): remove last separator of connection list * feat(ui): remove last slash for common list * feat(ui): update connection list by shared card list --------- Co-authored-by: Vu Van Duc --- src/ui/components/CardList/CardList.scss | 10 +++ src/ui/pages/Connections/Connections.scss | 87 ++++--------------- .../Connections/components/AlphabeticList.tsx | 44 +++++++--- .../Connections/components/ConnectionItem.tsx | 62 ------------- 4 files changed, 61 insertions(+), 142 deletions(-) delete mode 100644 src/ui/pages/Connections/components/ConnectionItem.tsx diff --git a/src/ui/components/CardList/CardList.scss b/src/ui/components/CardList/CardList.scss index edd0704a5..a0d73a434 100644 --- a/src/ui/components/CardList/CardList.scss +++ b/src/ui/components/CardList/CardList.scss @@ -18,6 +18,10 @@ --card-width: auto; --card-height: 2.75rem; + &:last-child { + --inner-border-width: 0; + } + .card-logo { border-radius: var(--logo-border-radius); width: var(--card-width); @@ -72,6 +76,12 @@ ion-item-sliding { --inner-padding-end: 0; + + &:last-child { + .card-item { + --inner-border-width: 0; + } + } } @media screen and (min-width: 250px) and (max-width: 370px) { diff --git a/src/ui/pages/Connections/Connections.scss b/src/ui/pages/Connections/Connections.scss index 0ab2a6ae3..eeb440e67 100644 --- a/src/ui/pages/Connections/Connections.scss +++ b/src/ui/pages/Connections/Connections.scss @@ -139,45 +139,18 @@ } } - ion-col { - &.connection-logo { - position: relative; - - img { - width: 2.75rem; - height: 2.75rem; - border-radius: 2rem; - } - } - - &.connection-info { - .connection-name { - font-size: 1rem; - margin-bottom: 0.31rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .connection-date { - font-size: 0.875rem; - color: var(--ion-color-secondary); - } - } - - ion-chip { - width: 6.4rem; - height: 1.9rem; - font-weight: 400; - margin: 0.3rem auto; - border-radius: 0.5rem; - padding-inline: 0.62rem; - - span { - text-transform: capitalize; - margin-right: 4px; - font-size: 0.8rem; - } + ion-chip { + width: 6.4rem; + height: 1.9rem; + font-weight: 400; + margin: 0.3rem auto; + border-radius: 0.5rem; + padding-inline: 0.62rem; + + span { + text-transform: capitalize; + margin-right: 4px; + font-size: 0.8rem; } } } @@ -211,36 +184,14 @@ } } - ion-col { - &.connection-logo { - position: relative; - - img { - width: 2.2rem; - height: 2.2rem; - } - } - - &.connection-info { - .connection-name { - font-size: 0.8rem; - margin-bottom: 0.248rem; - } - - .connection-date { - font-size: 0.7rem; - } - } - - ion-chip { - width: 5.12rem; - height: 1.52rem; - margin: 0.24rem auto; - padding: 0.496rem; + ion-chip { + width: 5.12rem; + height: 1.52rem; + margin: 0.24rem auto; + padding: 0.496rem; - span { - margin-right: 3px; - } + span { + margin-right: 3px; } } } diff --git a/src/ui/pages/Connections/components/AlphabeticList.tsx b/src/ui/pages/Connections/components/AlphabeticList.tsx index 5cd186195..14f92f7f7 100644 --- a/src/ui/pages/Connections/components/AlphabeticList.tsx +++ b/src/ui/pages/Connections/components/AlphabeticList.tsx @@ -1,5 +1,11 @@ +import { useMemo } from "react"; +import { hourglassOutline } from "ionicons/icons"; +import { IonChip, IonIcon } from "@ionic/react"; +import { CardItem, CardList } from "../../../components/CardList"; import { ConnectionShortDetails } from "../Connections.types"; -import { ConnectionItem } from "./ConnectionItem"; +import { formatShortDate } from "../../../utils/formatters"; +import KeriLogo from "../../../../ui/assets/images/KeriGeneric.jpg"; +import { ConnectionStatus } from "../../../../core/agent/agent.types"; const AlphabeticList = ({ items, @@ -8,18 +14,32 @@ const AlphabeticList = ({ items: ConnectionShortDetails[]; handleShowConnectionDetails: (item: ConnectionShortDetails) => void; }) => { + const displayConnection = useMemo((): CardItem[] => { + return items.map((connection) => ({ + id: connection.id, + title: connection.label as string, + subtitle: formatShortDate(`${connection?.connectionDate}`), + image: connection.logo || KeriLogo, + data: connection, + })); + }, [items]); + return ( - <> - {items.map((connection, index) => { - return ( - - ); - })} - + + data.status === ConnectionStatus.PENDING ? ( + + + {data.status} + + ) : null + } + /> ); }; diff --git a/src/ui/pages/Connections/components/ConnectionItem.tsx b/src/ui/pages/Connections/components/ConnectionItem.tsx deleted file mode 100644 index a9aa36f23..000000000 --- a/src/ui/pages/Connections/components/ConnectionItem.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { - IonItem, - IonGrid, - IonRow, - IonCol, - IonLabel, - IonChip, - IonIcon, -} from "@ionic/react"; -import { hourglassOutline } from "ionicons/icons"; -import { ConnectionStatus } from "../../../../core/agent/agent.types"; -import { formatShortDate } from "../../../utils/formatters"; -import { ConnectionItemProps } from "../Connections.types"; -import KeriLogo from "../../../../ui/assets/images/KeriGeneric.jpg"; - -const ConnectionItem = ({ - item, - handleShowConnectionDetails, -}: ConnectionItemProps) => { - return ( - handleShowConnectionDetails(item)}> - - - - connection-logo - - - {item?.label} - - {formatShortDate(`${item?.connectionDate}`)} - - - - {item.status === ConnectionStatus.PENDING && ( - - - {item.status} - - )} - - - - - ); -}; - -export { ConnectionItem }; From a59e7dc9484ad0a701fb59efa21a27395c7357ff Mon Sep 17 00:00:00 2001 From: Bao Hoang <142210850+bao-sotatek@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:55:43 +0700 Subject: [PATCH 12/28] feat(core): long running operation tracker for multi sig (#508) * feat(core): long running operation tracker for multi-sig * chore: refactor code * fix: remove unessessary code and add unittest --- src/core/agent/agent.ts | 7 ++- src/core/agent/agent.types.ts | 1 + .../records/operationPendingRecord.type.ts | 1 + .../agent/services/multiSigService.test.ts | 19 ++++++- src/core/agent/services/multiSigService.ts | 50 +++++++++++++------ .../services/signifyNotificationService.ts | 2 + .../components/AppWrapper/AppWrapper.test.tsx | 4 ++ src/ui/components/AppWrapper/AppWrapper.tsx | 1 + .../components/IdentifierStage0.test.tsx | 1 + .../components/IdentifierStage4.test.tsx | 1 + .../components/IdentifierStage4.tsx | 4 +- .../components/MultiSigRequest.tsx | 27 +++++----- 12 files changed, 87 insertions(+), 31 deletions(-) diff --git a/src/core/agent/agent.ts b/src/core/agent/agent.ts index d724a9ccc..1936e70ca 100644 --- a/src/core/agent/agent.ts +++ b/src/core/agent/agent.ts @@ -103,7 +103,8 @@ class Agent { this.multiSigService = new MultiSigService( this.agentServicesProps, this.identifierStorage, - this.notificationStorage + this.notificationStorage, + this.operationPendingStorage ); } return this.multiSigService; @@ -286,7 +287,9 @@ class Agent { this.peerConnectionStorage = new PeerConnectionStorage( this.getStorageService(this.storageSession) ); - + this.operationPendingStorage = new OperationPendingStorage( + this.getStorageService(this.storageSession) + ); this.agentServicesProps = { signifyClient: this.signifyClient, eventService: new EventService(), diff --git a/src/core/agent/agent.types.ts b/src/core/agent/agent.types.ts index 986f73d39..d2eca394a 100644 --- a/src/core/agent/agent.types.ts +++ b/src/core/agent/agent.types.ts @@ -130,6 +130,7 @@ interface AgentServicesProps { interface CreateIdentifierResult { signifyName: string; identifier: string; + isPending?: boolean; } interface IdentifierResult { diff --git a/src/core/agent/records/operationPendingRecord.type.ts b/src/core/agent/records/operationPendingRecord.type.ts index 4cdaa442c..2bfbaaf66 100644 --- a/src/core/agent/records/operationPendingRecord.type.ts +++ b/src/core/agent/records/operationPendingRecord.type.ts @@ -1,3 +1,4 @@ export enum OperationPendingRecordType { Witness = "witness", + Group = "group", } diff --git a/src/core/agent/services/multiSigService.test.ts b/src/core/agent/services/multiSigService.test.ts index d3e587327..ed93a97ed 100644 --- a/src/core/agent/services/multiSigService.test.ts +++ b/src/core/agent/services/multiSigService.test.ts @@ -101,6 +101,10 @@ const identifierStorage = jest.mocked({ createIdentifierMetadataRecord: jest.fn(), }); +const operationPendingStorage = jest.mocked({ + save: jest.fn(), +}); + const agentServicesProps = { signifyClient: signifyClient as any, eventService: new EventService(), @@ -109,7 +113,8 @@ const agentServicesProps = { const multiSigService = new MultiSigService( agentServicesProps, identifierStorage as any, - notificationStorage as any + notificationStorage as any, + operationPendingStorage as any ); let mockResolveOobi = jest.fn(); @@ -127,6 +132,9 @@ jest.mock("../../../core/agent/agent", () => ({ getIdentifiers: () => mockGetIdentifiers(), updateIdentifier: jest.fn(), }, + signifyNotifications: { + addPendingOperationToQueue: jest.fn(), + }, getKeriaOnlineStatus: jest.fn(), }, }, @@ -222,12 +230,15 @@ describe("Multisig sig service of agent", () => { ) ).toEqual({ identifier: multisigIdentifier, + isPending: true, signifyName: expect.any(String), }); expect(identifierStorage.createIdentifierMetadataRecord).toBeCalledWith( expect.objectContaining({ id: multisigIdentifier, isPending: true }) ); + expect(operationPendingStorage.save).toBeCalledTimes(1); + (keriMetadataRecord.groupMetadata as any).groupCreated = false; identifiersCreateMock.mockImplementation((name, _config) => { return { @@ -250,6 +261,7 @@ describe("Multisig sig service of agent", () => { ) ).toEqual({ identifier: `${multisigIdentifier}1`, + isPending: true, signifyName: expect.any(String), }); expect(identifierStorage.createIdentifierMetadataRecord).toBeCalledWith( @@ -281,6 +293,7 @@ describe("Multisig sig service of agent", () => { ) ).toEqual({ identifier: `${multisigIdentifier}2`, + isPending: true, signifyName: expect.any(String), }); expect(identifierStorage.createIdentifierMetadataRecord).toBeCalledWith( @@ -393,6 +406,7 @@ describe("Multisig sig service of agent", () => { ) ).toEqual({ identifier: multisigIdentifier, + isPending: true, signifyName: expect.any(String), }); }); @@ -482,8 +496,11 @@ describe("Multisig sig service of agent", () => { }) ).toEqual({ identifier: multisigIdentifier, + isPending: true, signifyName: expect.any(String), }); + + expect(operationPendingStorage.save).toBeCalledTimes(1); }); test("cannot join multisig by notification if exn messages are missing", async () => { diff --git a/src/core/agent/services/multiSigService.ts b/src/core/agent/services/multiSigService.ts index 5c30f026d..3a9ceca49 100644 --- a/src/core/agent/services/multiSigService.ts +++ b/src/core/agent/services/multiSigService.ts @@ -13,6 +13,7 @@ import { IdentifierMetadataRecordProps, IdentifierStorage, NotificationStorage, + OperationPendingStorage, } from "../records"; import { AgentService } from "./agentService"; import { MultiSigIcpRequestDetails } from "./identifier.types"; @@ -22,7 +23,8 @@ import { MultiSigExnMessage, CreateMultisigExnPayload, } from "./multiSig.types"; -import { OnlineOnly } from "./utils"; +import { OnlineOnly, waitAndGetDoneOp } from "./utils"; +import { OperationPendingRecordType } from "../records/operationPendingRecord.type"; class MultiSigService extends AgentService { static readonly INVALID_THRESHOLD = "Invalid threshold"; @@ -50,15 +52,18 @@ class MultiSigService extends AgentService { protected readonly identifierStorage: IdentifierStorage; protected readonly notificationStorage!: NotificationStorage; + protected readonly operationPendingStorage: OperationPendingStorage; constructor( agentServiceProps: AgentServicesProps, identifierStorage: IdentifierStorage, - notificationStorage: NotificationStorage + notificationStorage: NotificationStorage, + operationPendingStorage: OperationPendingStorage ) { super(agentServiceProps); this.identifierStorage = identifierStorage; this.notificationStorage = notificationStorage; + this.operationPendingStorage = operationPendingStorage; } @OnlineOnly @@ -116,11 +121,18 @@ class MultiSigService extends AgentService { threshold, delegateAid ); - const multisigId = result.op.name.split(".")[1]; - //this will be updated once the operation is done - let isPending = true; - if (result.op.done || threshold === 1) { - isPending = false; + const op = result.op; + const multisigId = op.name.split(".")[1]; + const isPending = !op.done; + + if (isPending) { + const pendingOperation = await this.operationPendingStorage.save({ + id: op.name, + recordType: OperationPendingRecordType.Group, + }); + Agent.agent.signifyNotifications.addPendingOperationToQueue( + pendingOperation + ); } await this.identifierStorage.createIdentifierMetadataRecord({ id: multisigId, @@ -136,7 +148,7 @@ class MultiSigService extends AgentService { ourMetadata.id, ourMetadata ); - return { identifier: multisigId, signifyName }; + return { identifier: multisigId, signifyName, isPending }; } private async createAidMultisig( @@ -402,17 +414,26 @@ class MultiSigService extends AgentService { const signifyName = uuidv4(); const res = await this.joinMultisigKeri(exn, aid, signifyName); await this.notificationStorage.deleteById(notificationId); - const multisigId = res.op.name.split(".")[1]; - let isPending = res.op.done ? false : true; //this will be updated once the operation is done - if (exn.e.icp.kt === "1") { - isPending = false; + const op = res.op; + const multisigId = op.name.split(".")[1]; + const isPending = !op.done; + + if (isPending) { + const pendingOperation = await this.operationPendingStorage.save({ + id: op.name, + recordType: OperationPendingRecordType.Group, + }); + Agent.agent.signifyNotifications.addPendingOperationToQueue( + pendingOperation + ); } + await this.identifierStorage.createIdentifierMetadataRecord({ id: multisigId, displayName: meta.displayName, theme: meta.theme, signifyName, - signifyOpName: res.op.name, //we save the signifyOpName here to sync the multisig's status later + signifyOpName: op.name, //we save the signifyOpName here to sync the multisig's status later isPending, multisigManageAid: identifier.id, }); @@ -421,7 +442,8 @@ class MultiSigService extends AgentService { identifier.id, identifier ); - return { identifier: multisigId, signifyName }; + + return { identifier: multisigId, signifyName, isPending }; } @OnlineOnly diff --git a/src/core/agent/services/signifyNotificationService.ts b/src/core/agent/services/signifyNotificationService.ts index c5a1aab8c..1be5c400b 100644 --- a/src/core/agent/services/signifyNotificationService.ts +++ b/src/core/agent/services/signifyNotificationService.ts @@ -275,6 +275,7 @@ class SignifyNotificationService extends AgentService { "" ); switch (pendingOperation.recordType) { + case OperationPendingRecordType.Group: case OperationPendingRecordType.Witness: { await this.identifierStorage.updateIdentifierMetadata( recordId, @@ -289,6 +290,7 @@ class SignifyNotificationService extends AgentService { break; } + default: break; } diff --git a/src/ui/components/AppWrapper/AppWrapper.test.tsx b/src/ui/components/AppWrapper/AppWrapper.test.tsx index 720ed15e7..2ca5d9a91 100644 --- a/src/ui/components/AppWrapper/AppWrapper.test.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.test.tsx @@ -390,6 +390,10 @@ describe("Signify operation state changed handler", () => { { opType: OperationPendingRecordType.Witness, oid: aid.id }, dispatch ); + await signifyOperationStateChangeHandler( + { opType: OperationPendingRecordType.Group, oid: aid.id }, + dispatch + ); expect(dispatch).toBeCalledWith( updateIsPending({ id: aid.id, isPending: false }) ); diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index b19eba019..e760f4490 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -204,6 +204,7 @@ const signifyOperationStateChangeHandler = async ( ) => { switch (opType) { case OperationPendingRecordType.Witness: + case OperationPendingRecordType.Group: dispatch(updateIsPending({ id: oid, isPending: false })); dispatch(setToastMsg(ToastMsgType.IDENTIFIER_UPDATED)); break; diff --git a/src/ui/components/CreateIdentifier/components/IdentifierStage0.test.tsx b/src/ui/components/CreateIdentifier/components/IdentifierStage0.test.tsx index aa2272a8b..2466697c0 100644 --- a/src/ui/components/CreateIdentifier/components/IdentifierStage0.test.tsx +++ b/src/ui/components/CreateIdentifier/components/IdentifierStage0.test.tsx @@ -46,6 +46,7 @@ jest.mock("../../../../core/agent/agent", () => ({ getIdentifiersCache: jest.fn(), createIdentifier: jest.fn(() => ({ identifier: "mock-id", + isPending: true, signifyName: "mock name", })), }, diff --git a/src/ui/components/CreateIdentifier/components/IdentifierStage4.test.tsx b/src/ui/components/CreateIdentifier/components/IdentifierStage4.test.tsx index 6fc647876..388f6d765 100644 --- a/src/ui/components/CreateIdentifier/components/IdentifierStage4.test.tsx +++ b/src/ui/components/CreateIdentifier/components/IdentifierStage4.test.tsx @@ -16,6 +16,7 @@ mockIonicReact(); const createMultiSignMock = jest.fn((...arg: any) => ({ identifier: "mock-id", + isPending: true, signifyName: "mock-name", })); diff --git a/src/ui/components/CreateIdentifier/components/IdentifierStage4.tsx b/src/ui/components/CreateIdentifier/components/IdentifierStage4.tsx index e8113c29a..204db594f 100644 --- a/src/ui/components/CreateIdentifier/components/IdentifierStage4.tsx +++ b/src/ui/components/CreateIdentifier/components/IdentifierStage4.tsx @@ -47,7 +47,7 @@ const IdentifierStage4 = ({ ); return; } else { - const { identifier, signifyName } = + const { identifier, signifyName, isPending } = await Agent.agent.multiSigs.createMultisig( ourIdentifier, otherIdentifierContacts, @@ -59,7 +59,7 @@ const IdentifierStage4 = ({ displayName: state.displayNameValue, createdAtUTC: new Date().toISOString(), theme: state.selectedTheme, - isPending: state.threshold >= 2, + isPending: !!isPending, signifyName, }; const filteredIdentifiersData = identifiersData.filter( diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/MultiSigRequest.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/MultiSigRequest.tsx index 5f44ee564..298d92f02 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/MultiSigRequest.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/MultiSigRequest.tsx @@ -30,6 +30,7 @@ import { PageHeader } from "../../../../../components/PageHeader"; import { i18n } from "../../../../../../i18n"; import { PageFooter } from "../../../../../components/PageFooter"; import "./MultiSigRequest.scss"; +import { CreateIdentifierResult } from "../../../../../../core/agent/agent.types"; const MultiSigRequest = ({ blur, @@ -51,23 +52,25 @@ const MultiSigRequest = ({ if (!(requestData.event && requestData.multisigIcpDetails)) { // Do some error thing here... maybe it's just a TODO } else { - const joinMultisigResult = await Agent.agent.multiSigs.joinMultisig( - requestData.event.id, - requestData.event.a.d as string, - { - theme: requestData.multisigIcpDetails.ourIdentifier.theme, - displayName: requestData.multisigIcpDetails.ourIdentifier.displayName, - } - ); + const { identifier, signifyName, isPending } = + (await Agent.agent.multiSigs.joinMultisig( + requestData.event.id, + requestData.event.a.d as string, + { + theme: requestData.multisigIcpDetails.ourIdentifier.theme, + displayName: + requestData.multisigIcpDetails.ourIdentifier.displayName, + } + )) as CreateIdentifierResult; - if (joinMultisigResult) { + if (identifier) { const newIdentifier: IdentifierShortDetails = { - id: joinMultisigResult.identifier, + id: identifier, displayName: requestData.multisigIcpDetails.ourIdentifier.displayName, createdAtUTC: `${requestData.event?.createdAt}`, theme: requestData.multisigIcpDetails.ourIdentifier.theme, - isPending: requestData.multisigIcpDetails.threshold >= 2, - signifyName: joinMultisigResult.signifyName, + isPending: !!isPending, + signifyName, }; const filteredIdentifiersData = identifiersData.filter( (item) => From 5385125c035e56ace46dfa08af4ce61f66a63f21 Mon Sep 17 00:00:00 2001 From: Sotatek-DukeVu <162310763+Sotatek-DukeVu@users.noreply.github.com> Date: Thu, 6 Jun 2024 19:17:07 +0700 Subject: [PATCH 13/28] feat(ui): CSS tweaks for Cardano Connect sign modal (#510) --- src/locales/en/en.json | 3 ++- .../CardDetailsAttributes.tsx | 1 + .../CardDetailsAttributes.types.ts | 1 + .../CardDetailsNestedAttributes.tsx | 14 +++++++++--- .../ScrollablePageLayout.tsx | 2 ++ .../ScrollablePageLayout.types.ts | 1 + .../components/SignRequest.scss | 22 ++++++++++++++++--- .../components/SignRequest.tsx | 18 +++++++++------ 8 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/locales/en/en.json b/src/locales/en/en.json index 6d8d4afc1..d6255ed9b 100644 --- a/src/locales/en/en.json +++ b/src/locales/en/en.json @@ -1272,7 +1272,8 @@ "accept": "Accept", "decline": "Decline", "addidentifier": "Add identifier", - "sign": "Sign" + "sign": "Sign", + "dontallow": "Don't allow" }, "alert": { "cancel": "Cancel" diff --git a/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsAttributes.tsx b/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsAttributes.tsx index 417c6f3ab..3fde40174 100644 --- a/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsAttributes.tsx +++ b/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsAttributes.tsx @@ -52,6 +52,7 @@ const CardDetailsAttributes = ({ key={index} attribute={item} customType={customType} + itemProps={itemProps} /> ); } diff --git a/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsAttributes.types.ts b/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsAttributes.types.ts index 0e537d302..8c83aa4ad 100644 --- a/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsAttributes.types.ts +++ b/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsAttributes.types.ts @@ -14,4 +14,5 @@ export interface CardDetailsNestedAttributesProps { attribute: [string, JSONValue]; customType?: string; cardKeyValue?: string; + itemProps?: Omit; } diff --git a/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsNestedAttributes.tsx b/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsNestedAttributes.tsx index ff09f2351..70dd117bd 100644 --- a/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsNestedAttributes.tsx +++ b/src/ui/components/CardDetails/CardDetailsAttributes/CardDetailsNestedAttributes.tsx @@ -12,7 +12,9 @@ const CardDetailsNestedAttributes = ({ attribute, cardKeyValue, customType, + itemProps, }: CardDetailsNestedAttributesProps) => { + const { className, ...restItemProps } = itemProps || {}; const key = attribute[0]; const item = attribute[1] as any; @@ -32,9 +34,13 @@ const CardDetailsNestedAttributes = ({ }, []); const isObjectItem = typeof item === "object" && item !== null; - const detailItemsClass = combineClassNames("card-details-attribute-item", { - "has-nested-item": isObjectItem, - }); + const detailItemsClass = combineClassNames( + "card-details-attribute-item", + className, + { + "has-nested-item": isObjectItem, + } + ); const infoTestId = item[10] === "T" ? "cred-detail-time" : undefined; const innerCardKeyValue = @@ -48,6 +54,7 @@ const CardDetailsNestedAttributes = ({ infoTestId={infoTestId} className={detailItemsClass} mask={false} + {...restItemProps} /> {isObjectItem && ( @@ -57,6 +64,7 @@ const CardDetailsNestedAttributes = ({ key={i} cardKeyValue={sub[0].replace(/([a-z])([A-Z])/g, "$1 $2") + ":"} attribute={sub} + itemProps={itemProps} /> ); })} diff --git a/src/ui/components/layout/ScrollablePageLayout/ScrollablePageLayout.tsx b/src/ui/components/layout/ScrollablePageLayout/ScrollablePageLayout.tsx index 39ebffc36..ef071b289 100644 --- a/src/ui/components/layout/ScrollablePageLayout/ScrollablePageLayout.tsx +++ b/src/ui/components/layout/ScrollablePageLayout/ScrollablePageLayout.tsx @@ -14,6 +14,7 @@ const ScrollablePageLayout = ({ activeStatus, children, customClass, + footer, }: ScrollablePageLayoutProps) => { const [isActive, setIsActive] = useState(false); useIonViewDidEnter(() => { @@ -39,6 +40,7 @@ const ScrollablePageLayout = ({ > {header} {children} + {footer} ); }; diff --git a/src/ui/components/layout/ScrollablePageLayout/ScrollablePageLayout.types.ts b/src/ui/components/layout/ScrollablePageLayout/ScrollablePageLayout.types.ts index 18873045b..ad592fca2 100644 --- a/src/ui/components/layout/ScrollablePageLayout/ScrollablePageLayout.types.ts +++ b/src/ui/components/layout/ScrollablePageLayout/ScrollablePageLayout.types.ts @@ -6,6 +6,7 @@ interface ScrollablePageLayoutProps { activeStatus?: boolean; children?: ReactNode; customClass?: string; + footer?: ReactNode; } export type { ScrollablePageLayoutProps }; diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.scss b/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.scss index 006d32a9d..be4f32051 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.scss +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.scss @@ -1,10 +1,22 @@ .sign-request { + .sign-request-header { + height: 44px; + } + & .page-header { ion-toolbar { --background: transparent; + + & > ion-title.md { + margin: 0; + } } } + .sign-footer { + padding: 1rem 0; + } + .sign-header { margin: 1.75rem 0; display: flex; @@ -35,8 +47,6 @@ } .sign-content { - margin-bottom: 3rem; - .sign-identifier { margin-bottom: 1.5rem; @@ -52,7 +62,13 @@ } .sign-data { - .sign-info-item { + .card-details-nested-content { + & > .card-details-info-block-inner { + padding: 1rem; + } + } + + & > .card-details-info-block-inner > .sign-info-item { &:first-child { margin-top: 0.75rem; } diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx index d30c5d113..326ca9f25 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx @@ -11,6 +11,7 @@ import CardanoLogo from "../../../../../assets/images/CardanoLogo.jpg"; import { RequestProps } from "../IncomingRequest.types"; import "./SignRequest.scss"; import { Spinner } from "../../../../../components/Spinner"; +import { PageHeader } from "../../../../../components/PageHeader"; const SignRequest = ({ pageId, @@ -47,7 +48,16 @@ const SignRequest = ({ activeStatus={activeStatus} pageId={pageId} customClass="sign-request" - header={

{`${i18n.t("request.sign.title")}`}

} + header={} + footer={ + + } >
- From 5a6c8989cf73c9dbd7b1cbab727a29c0c12924b2 Mon Sep 17 00:00:00 2001 From: Patrick Nguyen Date: Fri, 7 Jun 2024 18:48:25 +0700 Subject: [PATCH 14/28] test(core): peer connect unit tests (#509) * feat: peer connect unit tests * test: add error cases for connect and disconnect DApp --- package-lock.json | 95 +++++++++ package.json | 2 + .../agent/services/identifierService.test.ts | 37 ++++ .../identityWalletConnect.test.ts | 126 ++++++++++++ .../walletConnect/peerConnection.test.ts | 182 ++++++++++++++++++ .../cardano/walletConnect/peerConnection.ts | 1 - src/setupTests.ts | 8 + .../WalletConnect/WalletConnectStageTwo.tsx | 17 +- 8 files changed, 461 insertions(+), 7 deletions(-) create mode 100644 src/core/cardano/walletConnect/identityWalletConnect.test.ts create mode 100644 src/core/cardano/walletConnect/peerConnection.test.ts diff --git a/package-lock.json b/package-lock.json index e985a4bb1..35a77376a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "@capacitor/cli": "^5.0.0", "@faker-js/faker": "^8.4.1", "@ionic/react-test-utils": "^0.4.0", + "@peculiar/webcrypto": "^1.5.0", "@testing-library/dom": "^9.3.1", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^13.3.0", @@ -92,6 +93,7 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "expect-webdriverio": "^4.9.3", + "fake-indexeddb": "^6.0.0", "html-webpack-plugin": "^5.5.0", "husky": "^8.0.3", "jest": "^29.5.0", @@ -4945,6 +4947,45 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz", + "integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==", + "dev": true, + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz", + "integrity": "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==", + "dev": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2", + "webcrypto-core": "^1.8.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -21061,6 +21102,20 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, + "node_modules/asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "dev": true, + "dependencies": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", @@ -27196,6 +27251,15 @@ "node >=0.6.0" ] }, + "node_modules/fake-indexeddb": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.0.0.tgz", + "integrity": "sha512-YEboHE5VfopUclOck7LncgIqskAqnv4q0EWbYCaxKKjAvO93c+TJIaBuGy8CBFdbg9nKdpN3AuPRwVBJ4k7NrQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/fancy-log": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-2.0.0.tgz", @@ -38128,6 +38192,24 @@ } ] }, + "node_modules/pvtsutils": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", + "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", + "dev": true, + "dependencies": { + "tslib": "^2.6.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -43323,6 +43405,19 @@ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" }, + "node_modules/webcrypto-core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.0.tgz", + "integrity": "sha512-kR1UQNH8MD42CYuLzvibfakG5Ew5seG85dMMoAM/1LqvckxaF6pUiidLuraIu4V+YCIFabYecUZAW0TuxAoaqw==", + "dev": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^3.0.1", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2" + } + }, "node_modules/webdriver": { "version": "8.38.0", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.38.0.tgz", diff --git a/package.json b/package.json index ef23a18bd..099378222 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@capacitor/cli": "^5.0.0", "@faker-js/faker": "^8.4.1", "@ionic/react-test-utils": "^0.4.0", + "@peculiar/webcrypto": "^1.5.0", "@testing-library/dom": "^9.3.1", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^13.3.0", @@ -99,6 +100,7 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "expect-webdriverio": "^4.9.3", + "fake-indexeddb": "^6.0.0", "html-webpack-plugin": "^5.5.0", "husky": "^8.0.3", "jest": "^29.5.0", diff --git a/src/core/agent/services/identifierService.test.ts b/src/core/agent/services/identifierService.test.ts index 2bab5b507..cfd93f1ba 100644 --- a/src/core/agent/services/identifierService.test.ts +++ b/src/core/agent/services/identifierService.test.ts @@ -8,6 +8,19 @@ const identifiersListMock = jest.fn(); const identifiersGetMock = jest.fn(); const identifiersCreateMock = jest.fn(); const identifiersRotateMock = jest.fn(); +const mockSigner = { + _code: "A", + _size: -1, + _raw: {}, + _verfer: {}, +}; +const managerMock = { + get: () => { + return { + signers: [mockSigner], + }; + }, +}; const operationGetMock = jest.fn().mockImplementation((id: string) => { return { done: true, @@ -76,6 +89,7 @@ const signifyClient = jest.mocked({ query: jest.fn(), get: jest.fn(), }), + manager: undefined, }); const identifierStorage = jest.mocked({ getIdentifierMetadata: jest.fn(), @@ -479,6 +493,29 @@ describe("Single sig service of agent", () => { ).toStrictEqual(null); }); + test("Should throw error if we failed to obtain key manager when call getSigner", async () => { + Agent.agent.getKeriaOnlineStatus = jest.fn().mockReturnValueOnce(true); + identifierStorage.getIdentifierMetadata = jest + .fn() + .mockResolvedValue(keriMetadataRecord); + identifiersGetMock.mockResolvedValue(aidReturnedBySignify); + await expect( + identifierService.getSigner(keriMetadataRecord.id) + ).rejects.toThrowError(IdentifierService.FAILED_TO_OBTAIN_KEY_MANAGER); + }); + + test("Can get signer", async () => { + Agent.agent.getKeriaOnlineStatus = jest.fn().mockReturnValueOnce(true); + identifierStorage.getIdentifierMetadata = jest + .fn() + .mockResolvedValue(keriMetadataRecord); + identifiersGetMock.mockResolvedValue(aidReturnedBySignify); + signifyClient.manager = managerMock as any; + expect( + await identifierService.getSigner(keriMetadataRecord.id) + ).toStrictEqual(mockSigner); + }); + test("getIdentifier should throw an error when KERIA is offline", async () => { Agent.agent.getKeriaOnlineStatus = jest.fn().mockReturnValueOnce(false); await expect(identifierService.getIdentifier("id")).rejects.toThrowError( diff --git a/src/core/cardano/walletConnect/identityWalletConnect.test.ts b/src/core/cardano/walletConnect/identityWalletConnect.test.ts new file mode 100644 index 000000000..24e591ef8 --- /dev/null +++ b/src/core/cardano/walletConnect/identityWalletConnect.test.ts @@ -0,0 +1,126 @@ +import { Agent } from "../../agent/agent"; +import { + PeerConnectSigningEvent, + PeerConnectionEventTypes, + TxSignError, +} from "./peerConnection.types"; +import { EventService } from "../../agent/services/eventService"; +require("fake-indexeddb/auto"); +import { IdentityWalletConnect } from "./identityWalletConnect"; // Adjust the path accordingly + +jest.mock("../../agent/agent", () => ({ + Agent: { + agent: { + connections: { + getConnectionShortDetailById: jest.fn(), + getMultisigLinkedContacts: jest.fn(), + }, + identifiers: { + updateIdentifier: jest.fn(), + }, + getKeriaOnlineStatus: jest.fn(), + }, + }, +})); + +const walletInfo = { + name: "Test Wallet", + version: "1.0.0", + icon: "", + requestAutoconnect: false, +}; +const selectedAid = "ELToRvQwhQ299vCk9GFMhggSdLHAAarm6LG8tyemik9G"; +const seed = "test-seed"; +const announce = ["announce-1", "announce-2"]; +const eventServiceMock = new EventService(); +const identityWalletConnect = new IdentityWalletConnect( + walletInfo, + seed, + announce, + selectedAid, + eventServiceMock +); +describe("IdentityWalletConnect", () => { + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + test("should throw an error if identifier is not located", async () => { + Agent.agent.identifiers.getIdentifier = jest + .fn() + .mockResolvedValue(undefined); + + await expect(identityWalletConnect.getIdentifierOobi()).rejects.toThrow( + IdentityWalletConnect.IDENTIFIER_ID_NOT_LOCATED + ); + }); + + test("should return OOBI if identifier is located", async () => { + const mockIdentifier = { signifyName: "test-signify-name" }; + Agent.agent.identifiers.getIdentifier = jest + .fn() + .mockResolvedValue(mockIdentifier); + Agent.agent.connections.getOobi = jest.fn().mockResolvedValue("test-oobi"); + + const result = await identityWalletConnect.getIdentifierOobi(); + expect(result).toBe("test-oobi"); + }); + + test("should return connecting aid", () => { + expect(identityWalletConnect.getConnectingAid()).toBe(selectedAid); + }); + + test("should sign payload if approved", async () => { + const identifier = "test-identifier"; + const payload = "test-payload"; + const mockSigner = { + sign: jest.fn().mockReturnValue({ qb64: "signed-payload" }), + }; + + Agent.agent.identifiers.getSigner = jest.fn().mockResolvedValue(mockSigner); + eventServiceMock.emit = jest + .fn() + .mockImplementation((event: PeerConnectSigningEvent) => { + event.payload.approvalCallback(true); + }); + + const result = await identityWalletConnect.sign(identifier, payload); + expect(result).toBe("signed-payload"); + }); + + test("should return timeout error if signing takes too long", async () => { + const identifier = "test-identifier"; + const payload = "test-payload"; + + const mockApprovalCallback = jest.fn(); + + eventServiceMock.emit = jest.fn(); + jest.spyOn(global.Date, "now").mockImplementationOnce(() => 1); + const result = await identityWalletConnect.sign(identifier, payload); + expect(result).toEqual({ error: TxSignError.TimeOut }); + expect(eventServiceMock.emit).toHaveBeenCalledWith({ + type: PeerConnectionEventTypes.PeerConnectSign, + payload: { + identifier, + payload, + approvalCallback: expect.any(Function), + }, + }); + expect(mockApprovalCallback).not.toHaveBeenCalled(); + }); + + test("should return user declined error if approval is false", async () => { + const identifier = "test-identifier"; + const payload = "test-payload"; + + eventServiceMock.emit = jest + .fn() + .mockImplementation((event: PeerConnectSigningEvent) => { + event.payload.approvalCallback(false); + }); + + const result = await identityWalletConnect.sign(identifier, payload); + expect(result).toEqual({ error: TxSignError.UserDeclined }); + }); +}); diff --git a/src/core/cardano/walletConnect/peerConnection.test.ts b/src/core/cardano/walletConnect/peerConnection.test.ts new file mode 100644 index 000000000..7bc6c68b1 --- /dev/null +++ b/src/core/cardano/walletConnect/peerConnection.test.ts @@ -0,0 +1,182 @@ +import { DataType, SecureStorage } from "@aparajita/capacitor-secure-storage"; +import { IdentityWalletConnect } from "./identityWalletConnect"; +import { Agent } from "../../agent/agent"; +import { + PeerConnectionMetadataRecord, + PeerConnectionStorage, +} from "../../agent/records"; +import { PeerConnection } from "./peerConnection"; +import { KeyStoreKeys } from "../../storage"; +require("fake-indexeddb/auto"); + +jest.mock("../../agent/agent", () => ({ + Agent: { + agent: { + connections: { + getConnectionShortDetailById: jest.fn(), + getMultisigLinkedContacts: jest.fn(), + }, + identifiers: { + updateIdentifier: jest.fn(), + }, + getKeriaOnlineStatus: jest.fn(), + peerConnectionMetadataStorage: { + createPeerConnectionMetadataRecord: jest.fn(), + getPeerConnectionMetadata: jest.fn(), + }, + }, + }, +})); +const EXISTING_KEY = "keythatexists"; +const NON_EXISTING_KEY = "keythatdoesnotexist"; +const EXISTING_VALUE: DataType = "valuethatexists"; + +jest.mock("@aparajita/capacitor-secure-storage", () => ({ + SecureStorage: { + get: (key: string) => { + if (key === EXISTING_KEY) { + return EXISTING_VALUE; + } + return null; + }, + set: jest.fn(), + }, +})); + +describe("PeerConnection", () => { + let peerConnection: PeerConnection; + + beforeEach(() => { + peerConnection = PeerConnection.peerConnection; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should create a new PeerConnection instance", () => { + expect(peerConnection).toBeInstanceOf(PeerConnection); + }); + + test("should throw error if we call connect from a DApp when the peer connection hasn't started", async () => { + const dAppIdentifier = "testDApp"; + await expect( + peerConnection.connectWithDApp(dAppIdentifier) + ).rejects.toThrow(PeerConnection.PEER_CONNECTION_START_PENDING); + }); + + test("should throw error if we call disconnect from a DApp when the peer connection hasn't started", () => { + const dAppIdentifier = "testDApp"; + expect(() => peerConnection.disconnectDApp(dAppIdentifier)).toThrow( + PeerConnection.PEER_CONNECTION_START_PENDING + ); + }); + + test("should start a connection", async () => { + const selectedAid = "testAid"; + jest.spyOn(SecureStorage, "get").mockResolvedValue("seed"); + + await peerConnection.start(selectedAid); + + expect(SecureStorage.get).toHaveBeenCalledWith(KeyStoreKeys.MEERKAT_SEED); + }); + + test("should connect with a DApp if there is not existing connection", async () => { + const dAppIdentifier = "testDApp"; + Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata = jest + .fn() + .mockRejectedValue( + new Error(PeerConnectionStorage.PEER_CONNECTION_METADATA_RECORD_MISSING) + ); + const connectSpy = jest + .spyOn(IdentityWalletConnect.prototype, "connect") + .mockReturnValue("seed"); + + await peerConnection.start("testAid"); + await peerConnection.connectWithDApp(dAppIdentifier); + expect( + Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata + ).toHaveBeenCalledWith(dAppIdentifier); + expect( + Agent.agent.peerConnectionMetadataStorage + .createPeerConnectionMetadataRecord + ).toHaveBeenCalled(); + expect(connectSpy).toHaveBeenCalledWith(dAppIdentifier); + expect(SecureStorage.set).toHaveBeenCalledWith( + KeyStoreKeys.MEERKAT_SEED, + "seed" + ); + }); + + test("should connect with a DApp if there is an existing connection", async () => { + const dAppIdentifier = "testDApp"; + Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata = jest + .fn() + .mockResolvedValue({ + id: "id", + name: "name", + url: "url", + selectedAid: "aid", + iconB64: "icon", + } as PeerConnectionMetadataRecord); + const connectSpy = jest + .spyOn(IdentityWalletConnect.prototype, "connect") + .mockReturnValue("seed"); + + await peerConnection.start("testAid"); + await peerConnection.connectWithDApp(dAppIdentifier); + expect( + Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata + ).toHaveBeenCalledWith(dAppIdentifier); + expect( + Agent.agent.peerConnectionMetadataStorage + .createPeerConnectionMetadataRecord + ).not.toBeCalled(); + expect(connectSpy).toHaveBeenCalledWith(dAppIdentifier); + expect(SecureStorage.set).toHaveBeenCalledWith( + KeyStoreKeys.MEERKAT_SEED, + "seed" + ); + }); + + test("should throw an error if there is an error from getPeerConnectionMetadata", async () => { + const dAppIdentifier = "testDApp"; + Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata = jest + .fn() + .mockRejectedValueOnce(new Error("error")); + const connectSpy = jest + .spyOn(IdentityWalletConnect.prototype, "connect") + .mockReturnValue("seed"); + + await peerConnection.start("testAid"); + await expect( + peerConnection.connectWithDApp(dAppIdentifier) + ).rejects.toThrow(new Error("error")); + expect(connectSpy).not.toHaveBeenCalledWith(dAppIdentifier); + expect(SecureStorage.set).not.toHaveBeenCalledWith( + KeyStoreKeys.MEERKAT_SEED, + "seed" + ); + }); + + test("should disconnect from a DApp", () => { + const dAppIdentifier = "testDApp"; + const disconnectSpy = jest + .spyOn(IdentityWalletConnect.prototype, "disconnect") + .mockReturnValue(); + + peerConnection.start("testAid"); + peerConnection.disconnectDApp(dAppIdentifier); + expect(disconnectSpy).toHaveBeenCalledWith(dAppIdentifier); + }); + + test("should return the connected DApp address", () => { + peerConnection.start("testAid"); + expect(peerConnection.getConnectedDAppAddress()).toBe(""); + }); + + test("should return the connecting Aid", () => { + peerConnection.start("testAid"); + expect(peerConnection.getConnectingAid()).toBe("testAid"); + }); +}); diff --git a/src/core/cardano/walletConnect/peerConnection.ts b/src/core/cardano/walletConnect/peerConnection.ts index 421281dfe..8e0b07db4 100644 --- a/src/core/cardano/walletConnect/peerConnection.ts +++ b/src/core/cardano/walletConnect/peerConnection.ts @@ -205,7 +205,6 @@ class PeerConnection { if (this.identityWalletConnect === undefined) { throw new Error(PeerConnection.PEER_CONNECTION_START_PENDING); } - this.identityWalletConnect.disconnect(dAppIdentifier); if (isBroken) { diff --git a/src/setupTests.ts b/src/setupTests.ts index a2233d19b..2bf6facc3 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -3,9 +3,17 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import "@testing-library/jest-dom"; +import { Crypto } from "@peculiar/webcrypto"; // eslint-disable-next-line @typescript-eslint/no-var-requires const { TextDecoder, TextEncoder, ReadableStream } = require("node:util"); Reflect.set(globalThis, "TextDecoder", TextDecoder); Reflect.set(globalThis, "TextEncoder", TextEncoder); Reflect.set(globalThis, "ReadableStream", { ...ReadableStream, prototype: {} }); + +Object.defineProperty(global, "crypto", { + value: new Crypto(), + writable: true, +}); + +global.structuredClone = (v) => JSON.parse(JSON.stringify(v)); diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx index ef8a109fe..5c8d1573d 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx @@ -57,13 +57,18 @@ const WalletConnectStageTwo = ({ if (selectedIdentifier && pendingDAppMeerkat) { await PeerConnection.peerConnection.start(selectedIdentifier.id); await PeerConnection.peerConnection.connectWithDApp(pendingDAppMeerkat); - // Refresh the connections list - dispatch( - setWalletConnectionsCache([ - { id: pendingDAppMeerkat, selectedAid: selectedIdentifier.id }, - ...existingConnections, - ]) + const existingConnection = existingConnections.find( + (connection) => connection.id === pendingDAppMeerkat ); + if (!existingConnection) { + // Insert a new connection if needed + dispatch( + setWalletConnectionsCache([ + { id: pendingDAppMeerkat, selectedAid: selectedIdentifier.id }, + ...existingConnections, + ]) + ); + } } onClose(); } catch (e) { From 83f044755888a68e38562e63d9849c4fb17849e8 Mon Sep 17 00:00:00 2001 From: Patrick Nguyen Date: Sat, 8 Jun 2024 00:34:26 +0700 Subject: [PATCH 15/28] refactor: format peerConnection data (#511) * refactor: convert peerConnectionMetadata to avoid non-serialisation warnings * refactor: ignore serialisation check for signTransaction callback --- .../records/peerConnectionStorage.test.ts | 38 +++++++++++++++---- .../agent/records/peerConnectionStorage.ts | 26 +++++++++++-- .../cardano/walletConnect/peerConnection.ts | 2 +- .../walletConnect/peerConnection.types.ts | 10 +++++ src/store/index.ts | 9 +++++ .../walletConnectionsCache.types.ts | 2 +- .../components/AppWrapper/AppWrapper.test.tsx | 3 +- src/ui/components/AppWrapper/AppWrapper.tsx | 2 +- 8 files changed, 77 insertions(+), 15 deletions(-) diff --git a/src/core/agent/records/peerConnectionStorage.test.ts b/src/core/agent/records/peerConnectionStorage.test.ts index b01458d30..b67e73474 100644 --- a/src/core/agent/records/peerConnectionStorage.test.ts +++ b/src/core/agent/records/peerConnectionStorage.test.ts @@ -36,18 +36,26 @@ describe("Connection service of agent", () => { jest.resetAllMocks(); }); - test("Should get all credentials", async () => { + test("Should get all peer connection", async () => { storageService.getAll.mockResolvedValue([ peerConnectionMetadataRecord, peerConnectionMetadataRecord2, ]); - expect(await peerConnectionStorage.getAllPeerConnectionMetadata()).toEqual([ - peerConnectionMetadataRecord, - peerConnectionMetadataRecord2, - ]); + expect(await peerConnectionStorage.getAllPeerConnectionMetadata()).toEqual( + [peerConnectionMetadataRecord, peerConnectionMetadataRecord2].map( + (record) => ({ + id: record.id, + iconB64: record.iconB64, + name: record.name, + selectedAid: record.selectedAid, + url: record.url, + createdAt: record.createdAt.toISOString(), + }) + ) + ); }); - test("Should get credential metadata", async () => { + test("Should get peer connection meta data record", async () => { storageService.findById.mockResolvedValue(peerConnectionMetadataRecord); expect( await peerConnectionStorage.getPeerConnectionMetadata( @@ -56,6 +64,22 @@ describe("Connection service of agent", () => { ).toEqual(peerConnectionMetadataRecord); }); + test("Should get peer connection", async () => { + storageService.findById.mockResolvedValue(peerConnectionMetadataRecord); + expect( + await peerConnectionStorage.getPeerConnection( + peerConnectionMetadataRecord.id + ) + ).toEqual({ + id: peerConnectionMetadataRecord.id, + iconB64: peerConnectionMetadataRecord.iconB64, + name: peerConnectionMetadataRecord.name, + selectedAid: peerConnectionMetadataRecord.selectedAid, + url: peerConnectionMetadataRecord.url, + createdAt: peerConnectionMetadataRecord.createdAt.toISOString(), + }); + }); + test("Should throw if peerConnection metadata record is missing", async () => { storageService.findById.mockResolvedValue(null); await expect( @@ -74,7 +98,7 @@ describe("Connection service of agent", () => { expect(storageService.save).toBeCalledWith(peerConnectionMetadataRecord); }); - test("Should update credential metadata record", async () => { + test("Should update peer connection metadata record", async () => { storageService.findById.mockResolvedValue(peerConnectionMetadataRecord); await peerConnectionStorage.updatePeerConnectionMetadata( peerConnectionMetadataRecord.id, diff --git a/src/core/agent/records/peerConnectionStorage.ts b/src/core/agent/records/peerConnectionStorage.ts index 6d4954e6d..a1300095b 100644 --- a/src/core/agent/records/peerConnectionStorage.ts +++ b/src/core/agent/records/peerConnectionStorage.ts @@ -1,3 +1,4 @@ +import { PeerConnection } from "../../cardano/walletConnect/peerConnection.types"; import { StorageService } from "../../storage/storage.types"; import { PeerConnectionMetadataRecord, @@ -13,6 +14,18 @@ class PeerConnectionStorage { this.storageService = storageService; } + async getPeerConnection(id: string): Promise { + const metadata = await this.getPeerConnectionMetadata(id); + return { + id: metadata.id, + iconB64: metadata.iconB64, + name: metadata.name, + selectedAid: metadata.selectedAid, + url: metadata.url, + createdAt: metadata.createdAt.toISOString(), + }; + } + async getPeerConnectionMetadata( id: string ): Promise { @@ -28,13 +41,18 @@ class PeerConnectionStorage { return metadata; } - async getAllPeerConnectionMetadata(): Promise< - PeerConnectionMetadataRecord[] - > { + async getAllPeerConnectionMetadata(): Promise { const records = await this.storageService.getAll( PeerConnectionMetadataRecord ); - return records; + return records.map((record) => ({ + id: record.id, + iconB64: record.iconB64, + name: record.name, + selectedAid: record.selectedAid, + url: record.url, + createdAt: record.createdAt.toISOString(), + })); } async updatePeerConnectionMetadata( diff --git a/src/core/cardano/walletConnect/peerConnection.ts b/src/core/cardano/walletConnect/peerConnection.ts index 8e0b07db4..28a41da6d 100644 --- a/src/core/cardano/walletConnect/peerConnection.ts +++ b/src/core/cardano/walletConnect/peerConnection.ts @@ -155,7 +155,7 @@ class PeerConnection { this.eventService.emit({ type: PeerConnectionEventTypes.PeerDisconnected, payload: { - dAppAddress: disConnectMessage.address as string, + dAppAddress: disConnectMessage.dApp.address as string, }, }); } diff --git a/src/core/cardano/walletConnect/peerConnection.types.ts b/src/core/cardano/walletConnect/peerConnection.types.ts index f92b47ef1..c8025a72d 100644 --- a/src/core/cardano/walletConnect/peerConnection.types.ts +++ b/src/core/cardano/walletConnect/peerConnection.types.ts @@ -52,6 +52,15 @@ interface PeerConnectionError { info: string; } +interface PeerConnection { + id: string; + name?: string; + url?: string; + iconB64?: string; + selectedAid?: string; + createdAt?: string; +} + export const TxSignError: { [key: string]: PeerConnectionError } = { ProofGeneration: { code: 1, @@ -69,4 +78,5 @@ export type { PeerDisconnectedEvent, PeerConnectionBrokenEvent, PeerConnectionError, + PeerConnection, }; diff --git a/src/store/index.ts b/src/store/index.ts index 5af20dd5c..fd9d5a908 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -23,6 +23,15 @@ const store = configureStore({ biometryCache: biometryCacheSlice.reducer, ssiAgentCache: ssiAgentSlice.reducer, }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + // Ignore these field paths in all actions + ignoredActionPaths: [ + "payload.signTransaction.payload.approvalCallback", + ], + }, + }), }); type RootState = ReturnType; diff --git a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts index c0adba811..47fba1e9a 100644 --- a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts +++ b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts @@ -2,7 +2,7 @@ interface ConnectionData { id: string; name?: string; url?: string; - createdAt?: Date; + createdAt?: string; iconB64?: string; selectedAid?: string; } diff --git a/src/ui/components/AppWrapper/AppWrapper.test.tsx b/src/ui/components/AppWrapper/AppWrapper.test.tsx index 2ca5d9a91..10000de84 100644 --- a/src/ui/components/AppWrapper/AppWrapper.test.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.test.tsx @@ -106,6 +106,7 @@ jest.mock("../../../core/agent/agent", () => ({ peerConnectionMetadataStorage: { getAllPeerConnectionMetadata: jest.fn(), getPeerConnectionMetadata: jest.fn(), + getPeerConnection: jest.fn(), }, basicStorage: { findById: jest.fn(), @@ -346,7 +347,7 @@ describe("AppWrapper handler", () => { }); test("handle peer sign request event", async () => { - Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata = jest + Agent.agent.peerConnectionMetadataStorage.getPeerConnection = jest .fn() .mockResolvedValue(peerConnectionMock); await peerConnectRequestSignChangeHandler( diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index e760f4490..d2bd21ea1 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -155,7 +155,7 @@ const peerConnectRequestSignChangeHandler = async ( const connectedDAppAddress = PeerConnection.peerConnection.getConnectedDAppAddress(); const peerConnection = - await Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata( + await Agent.agent.peerConnectionMetadataStorage.getPeerConnection( connectedDAppAddress ); dispatch( From 1e8dff573b1156e4b6151761ea7c3d96f76c500e Mon Sep 17 00:00:00 2001 From: Sotatek-DukeVu <162310763+Sotatek-DukeVu@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:34:15 +0700 Subject: [PATCH 16/28] feat(ui): auto-open the modal for the connection (#512) * feat(ui): open connection modal after create new connect * feat(ui): responsive new design * fix(ui): fix open pending connection * fix(ui): resolve review comment * fix(ui): resolve review comment * fix(ui): change duration to constant --------- Co-authored-by: Vu Van Duc --- .../cardano/walletConnect/peerConnection.ts | 3 +- src/locales/en/en.json | 18 +- src/routes/backRoute/backRoute.test.ts | 4 +- src/routes/nextRoute/nextRoute.test.ts | 4 +- .../walletConnectionsCache.test.ts | 30 +-- .../walletConnectionsCache.ts | 22 ++- .../walletConnectionsCache.types.ts | 4 +- .../components/AppWrapper/AppWrapper.test.tsx | 6 +- src/ui/components/AppWrapper/AppWrapper.tsx | 14 +- src/ui/components/CardList/CardList.scss | 5 + src/ui/components/Scanner/Scanner.tsx | 8 +- src/ui/components/SideSlider/SideSlider.tsx | 4 +- .../components/SideSlider/SideSlider.types.ts | 2 + src/ui/globals/types.ts | 1 + .../ConfirmConnectModal.scss | 32 +++- .../ConfirmConnectModal.test.tsx | 72 +++++-- .../ConfirmConnectModal.tsx | 89 ++++++--- .../ConnectWallet/ConnectWallet.scss | 10 + .../ConnectWallet/ConnectWallet.test.tsx | 169 +++++++++++++++- .../ConnectWallet/ConnectWallet.tsx | 180 ++++++++++++++---- .../ConnectWallet/ConnectWallet.types.ts | 2 +- src/ui/pages/SidePage/SidePage.test.tsx | 4 +- src/ui/pages/SidePage/SidePage.tsx | 6 +- .../IncomingRequest/IncomingRequest.tsx | 3 +- .../WalletConnect/WalletConnect.test.tsx | 3 +- .../WalletConnect/WalletConnect.tsx | 32 +--- .../WalletConnect/WalletConnectStageOne.tsx | 8 + .../WalletConnect/WalletConnectStageTwo.tsx | 20 +- src/ui/utils/formatters.test.ts | 6 +- src/ui/utils/formatters.ts | 5 + 30 files changed, 604 insertions(+), 162 deletions(-) diff --git a/src/core/cardano/walletConnect/peerConnection.ts b/src/core/cardano/walletConnect/peerConnection.ts index 28a41da6d..1fd90b94c 100644 --- a/src/core/cardano/walletConnect/peerConnection.ts +++ b/src/core/cardano/walletConnect/peerConnection.ts @@ -119,7 +119,7 @@ class PeerConnection { if (!connectMessage.error) { const { name, url, address, icon } = connectMessage.dApp; this.connectedDAppAdress = address; - let iconB64 = ICON_BASE64; + let iconB64; // Check if the icon is base64 if ( icon && @@ -192,7 +192,6 @@ class PeerConnection { { id: dAppIdentifier, selectedAid: this.identityWalletConnect.getConnectingAid(), - iconB64: ICON_BASE64, } ); } diff --git a/src/locales/en/en.json b/src/locales/en/en.json index d6255ed9b..9296d6bcc 100644 --- a/src/locales/en/en.json +++ b/src/locales/en/en.json @@ -898,16 +898,10 @@ "cip": "CIP-45", "tabheader": "Cardano Connect", "connectbtn": "Connect with Cardano", - "connectwalletmodal": { - "header": "New connection", - "cancel": "Cancel", - "scanqr": "Scan QR code", - "pastePID": "Paste Peer ID", - "disconnectbeforecreatealert": { - "message": "You are currently connected. To connect with a new wallet, you will be disconnected from your current selection. Would you like to disconnect and continue?", - "confirm": "Continue", - "cancel": "Cancel" - } + "disconnectbeforecreatealert": { + "message": "You are currently connected. To connect with a new wallet, you will be disconnected from your current selection. Would you like to disconnect and continue?", + "confirm": "Continue", + "cancel": "Cancel" }, "connectionbrokenalert": { "message": "Your connection has been disconnected as you have deleted the chosen identifier paired with this connection. Please choose a new identifier to re-establish this connection.", @@ -937,7 +931,9 @@ "confirmconnect": { "done": "Done", "connectbtn": "Connect", - "disconnectbtn": "Disconnect" + "disconnectbtn": "Disconnect", + "connectingbtn": "Connecting...", + "pending": "Pending" } }, "request": { diff --git a/src/routes/backRoute/backRoute.test.ts b/src/routes/backRoute/backRoute.test.ts index 59f65fe7e..5e9ee7da9 100644 --- a/src/routes/backRoute/backRoute.test.ts +++ b/src/routes/backRoute/backRoute.test.ts @@ -62,7 +62,7 @@ describe("getBackRoute", () => { walletConnectionsCache: { walletConnections: [], connectedWallet: null, - pendingDAppMeerKat: null, + pendingConnection: null, }, identifierViewTypeCacheCache: { viewType: null, @@ -189,7 +189,7 @@ describe("getPreviousRoute", () => { walletConnectionsCache: { walletConnections: [], connectedWallet: null, - pendingDAppMeerKat: null, + pendingConnection: null, }, identifierViewTypeCacheCache: { viewType: null, diff --git a/src/routes/nextRoute/nextRoute.test.ts b/src/routes/nextRoute/nextRoute.test.ts index 0ed8e742a..9c1d03d03 100644 --- a/src/routes/nextRoute/nextRoute.test.ts +++ b/src/routes/nextRoute/nextRoute.test.ts @@ -61,7 +61,7 @@ describe("NextRoute", () => { walletConnectionsCache: { walletConnections: [], connectedWallet: null, - pendingDAppMeerKat: null, + pendingConnection: null, }, identifierViewTypeCacheCache: { viewType: null, @@ -194,7 +194,7 @@ describe("getNextRoute", () => { walletConnectionsCache: { walletConnections: [], connectedWallet: null, - pendingDAppMeerKat: null, + pendingConnection: null, }, identifierViewTypeCacheCache: { viewType: null, diff --git a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.test.ts b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.test.ts index 7e1ffc6bf..3abadbcf2 100644 --- a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.test.ts +++ b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.test.ts @@ -2,10 +2,10 @@ import { PayloadAction } from "@reduxjs/toolkit"; import { RootState } from "../../index"; import { getConnectedWallet, - getPendingDAppMeerkat, + getPendingConnection, getWalletConnectionsCache, setConnectedWallet, - setPendingDAppMeerKat, + setPendingConnection, setWalletConnectionsCache, walletConnectionsCacheSlice, } from "./walletConnectionsCache"; @@ -18,7 +18,7 @@ describe("walletConnectionsCacheSlice", () => { const initialState: WalletConnectState = { walletConnections: [], connectedWallet: null, - pendingDAppMeerKat: null, + pendingConnection: null, }; it("should return the initial state", () => { @@ -51,16 +51,18 @@ describe("walletConnectionsCacheSlice", () => { }; const newState = walletConnectionsCacheSlice.reducer( initialState, - setConnectedWallet(connection.id) + setConnectedWallet(connection) ); - expect(newState.connectedWallet).toEqual(connection.id); + expect(newState.connectedWallet).toEqual(connection); }); - it("should handle setPendingDAppMeerKat", () => { + it("should handle setPendingConnection", () => { const newState = walletConnectionsCacheSlice.reducer( initialState, - setPendingDAppMeerKat("pending-meerkat") + setPendingConnection({ + id: "pending-meerkat", + }) ); - expect(newState.pendingDAppMeerKat).toEqual("pending-meerkat"); + expect(newState.pendingConnection?.id).toEqual("pending-meerkat"); }); }); @@ -94,7 +96,9 @@ describe("Get wallet connections cache", () => { it("should return connected wallet from RootState", () => { const state = { walletConnectionsCache: { - connectedWallet: "1", + connectedWallet: { + id: "1", + }, }, } as RootState; const connectionCache = getConnectedWallet(state); @@ -105,12 +109,14 @@ describe("Get wallet connections cache", () => { it("should return pending DApp MeerKat from RootState", () => { const state = { walletConnectionsCache: { - pendingDAppMeerKat: "pending-meerkat", + pendingConnection: { + id: "pending-meerkat", + }, }, } as RootState; - const pendingMeerKatCache = getPendingDAppMeerkat(state); + const pendingMeerKatCache = getPendingConnection(state); expect(pendingMeerKatCache).toEqual( - state.walletConnectionsCache.pendingDAppMeerKat + state.walletConnectionsCache.pendingConnection ); }); }); diff --git a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.ts b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.ts index 1526d8502..27feabff5 100644 --- a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.ts +++ b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.ts @@ -8,7 +8,7 @@ import { const initialState: WalletConnectState = { walletConnections: [], connectedWallet: null, - pendingDAppMeerKat: null, + pendingConnection: null, }; const walletConnectionsCacheSlice = createSlice({ name: "walletConnectionsCache", @@ -20,11 +20,17 @@ const walletConnectionsCacheSlice = createSlice({ ) => { state.walletConnections = action.payload; }, - setConnectedWallet: (state, action: PayloadAction) => { + setConnectedWallet: ( + state, + action: PayloadAction + ) => { state.connectedWallet = action.payload; }, - setPendingDAppMeerKat: (state, action: PayloadAction) => { - state.pendingDAppMeerKat = action.payload; + setPendingConnection: ( + state, + action: PayloadAction + ) => { + state.pendingConnection = action.payload; }, }, }); @@ -34,7 +40,7 @@ export { initialState, walletConnectionsCacheSlice }; export const { setWalletConnectionsCache, setConnectedWallet, - setPendingDAppMeerKat, + setPendingConnection, } = walletConnectionsCacheSlice.actions; const getWalletConnectionsCache = (state: RootState) => @@ -43,7 +49,7 @@ const getWalletConnectionsCache = (state: RootState) => const getConnectedWallet = (state: RootState) => state.walletConnectionsCache.connectedWallet; -const getPendingDAppMeerkat = (state: RootState) => - state.walletConnectionsCache.pendingDAppMeerKat; +const getPendingConnection = (state: RootState) => + state.walletConnectionsCache.pendingConnection; -export { getWalletConnectionsCache, getConnectedWallet, getPendingDAppMeerkat }; +export { getWalletConnectionsCache, getConnectedWallet, getPendingConnection }; diff --git a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts index 47fba1e9a..59ff811dd 100644 --- a/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts +++ b/src/store/reducers/walletConnectionsCache/walletConnectionsCache.types.ts @@ -9,8 +9,8 @@ interface ConnectionData { interface WalletConnectState { walletConnections: ConnectionData[]; - connectedWallet: string | null; - pendingDAppMeerKat: string | null; + connectedWallet: ConnectionData | null; + pendingConnection: ConnectionData | null; } export type { ConnectionData, WalletConnectState }; diff --git a/src/ui/components/AppWrapper/AppWrapper.test.tsx b/src/ui/components/AppWrapper/AppWrapper.test.tsx index 10000de84..e6f486d1c 100644 --- a/src/ui/components/AppWrapper/AppWrapper.test.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.test.tsx @@ -323,9 +323,9 @@ describe("AppWrapper handler", () => { Agent.agent.peerConnectionMetadataStorage.getAllPeerConnectionMetadata = jest.fn().mockResolvedValue([peerConnectionMock]); await peerConnectedChangeHandler(peerConnectedEventMock, dispatch); - expect(dispatch).toBeCalledWith( - setConnectedWallet(peerConnectionMock.id) - ); + await waitFor(() => { + expect(dispatch).toBeCalledWith(setConnectedWallet(peerConnectionMock)); + }); expect(dispatch).toBeCalledWith( setWalletConnectionsCache([peerConnectionMock]) ); diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index d2bd21ea1..959688426 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -2,7 +2,6 @@ import { ReactNode, useEffect, useState } from "react"; import { useAppDispatch, useAppSelector } from "../../../store/hooks"; import { getAuthentication, - getCurrentOperation, setAuthentication, setCurrentOperation, setInitialized, @@ -45,6 +44,7 @@ import { useActivityTimer } from "./hooks/useActivityTimer"; import { getConnectedWallet, setConnectedWallet, + setPendingConnection, setWalletConnectionsCache, } from "../../../store/reducers/walletConnectionsCache"; import { PeerConnection } from "../../../core/cardano/walletConnect/peerConnection"; @@ -59,7 +59,6 @@ import { setViewTypeCache } from "../../../store/reducers/identifierViewTypeCach import { CardListViewType } from "../SwitchCardView"; import { setEnableBiometryCache } from "../../../store/reducers/biometryCache"; import { setCredsArchivedCache } from "../../../store/reducers/credsArchivedCache"; -import { IdentifierShortDetails } from "../../../core/agent/services/identifier.types"; import { OperationPendingRecordType } from "../../../core/agent/records/operationPendingRecord.type"; import { i18n } from "../../../i18n"; import { Alert } from "../Alert"; @@ -175,7 +174,13 @@ const peerConnectedChangeHandler = async ( const existingConnections = await Agent.agent.peerConnectionMetadataStorage.getAllPeerConnectionMetadata(); dispatch(setWalletConnectionsCache(existingConnections)); - dispatch(setConnectedWallet(event.payload.dAppAddress)); + const connectedWallet = existingConnections.find( + (connection) => connection.id === event.payload.dAppAddress + ); + if (connectedWallet) { + dispatch(setConnectedWallet(connectedWallet)); + } + dispatch(setPendingConnection(null)); dispatch(setToastMsg(ToastMsgType.CONNECT_WALLET_SUCCESS)); }; @@ -214,7 +219,6 @@ const signifyOperationStateChangeHandler = async ( const AppWrapper = (props: { children: ReactNode }) => { const dispatch = useAppDispatch(); const authentication = useAppSelector(getAuthentication); - const operation = useAppSelector(getCurrentOperation); const connectedWallet = useAppSelector(getConnectedWallet); const [isOnline, setIsOnline] = useState(false); const [isMessagesHandled, setIsMessagesHandled] = useState(false); @@ -414,7 +418,7 @@ const AppWrapper = (props: { children: ReactNode }) => { if (connectedWallet) { return peerDisconnectedChangeHandler( event, - connectedWallet, + connectedWallet.id, dispatch ); } diff --git a/src/ui/components/CardList/CardList.scss b/src/ui/components/CardList/CardList.scss index a0d73a434..a529f1595 100644 --- a/src/ui/components/CardList/CardList.scss +++ b/src/ui/components/CardList/CardList.scss @@ -8,6 +8,7 @@ ion-item.card-item { --inner-padding-top: 1rem; --inner-padding-bottom: 1rem; + --inner-padding-end: 0; --padding-start: 0; --background: var(--ion-color-light); --title-font-size: 1rem; @@ -43,6 +44,7 @@ .card-info { flex: 1; + overflow: hidden; .card-title { font-size: var(--title-font-size); @@ -51,6 +53,9 @@ font-weight: 500; color: var(--ion-color-primary); margin-top: 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; &.no-margin { --title-margin-bottom: 0; diff --git a/src/ui/components/Scanner/Scanner.tsx b/src/ui/components/Scanner/Scanner.tsx index c6195ad6b..768461253 100644 --- a/src/ui/components/Scanner/Scanner.tsx +++ b/src/ui/components/Scanner/Scanner.tsx @@ -35,7 +35,7 @@ import { MultiSigGroup } from "../../../store/reducers/identifiersCache/identifi import { PageFooter } from "../PageFooter"; import { CustomInput } from "../CustomInput"; import { OptionModal } from "../OptionsModal"; -import { setPendingDAppMeerKat } from "../../../store/reducers/walletConnectionsCache"; +import { setPendingConnection } from "../../../store/reducers/walletConnectionsCache"; import { CreateIdentifier } from "../CreateIdentifier"; import { setBootUrl, setConnectUrl } from "../../../store/reducers/ssiAgent"; @@ -96,7 +96,11 @@ const Scanner = forwardRef( const handleConnectWallet = (id: string) => { handleReset && handleReset(); dispatch(setToastMsg(ToastMsgType.PEER_ID_SUCCESS)); - dispatch(setPendingDAppMeerKat(id)); + dispatch( + setPendingConnection({ + id, + }) + ); }; const updateConnections = async (groupId: string) => { diff --git a/src/ui/components/SideSlider/SideSlider.tsx b/src/ui/components/SideSlider/SideSlider.tsx index 35c59c8b0..0fb456b79 100644 --- a/src/ui/components/SideSlider/SideSlider.tsx +++ b/src/ui/components/SideSlider/SideSlider.tsx @@ -1,5 +1,5 @@ import { IonModal, createAnimation } from "@ionic/react"; -import { SideSliderProps } from "./SideSlider.types"; +import { ANIMATION_DURATION, SideSliderProps } from "./SideSlider.types"; import { combineClassNames } from "../../utils/style"; import "./SideSlider.scss"; @@ -21,7 +21,7 @@ const SideSlider = ({ return createAnimation() .addElement(modalWrapper) .easing("ease-out") - .duration(500) + .duration(ANIMATION_DURATION) .fromTo("transform", "translateX(100%)", "translateX(0)") .fromTo("opacity", 1, 1) .afterStyles({ diff --git a/src/ui/components/SideSlider/SideSlider.types.ts b/src/ui/components/SideSlider/SideSlider.types.ts index b6483ce68..6cfa3fee6 100644 --- a/src/ui/components/SideSlider/SideSlider.types.ts +++ b/src/ui/components/SideSlider/SideSlider.types.ts @@ -10,4 +10,6 @@ interface SideSliderProps { onCloseAnimationEnd?: () => void; } +export const ANIMATION_DURATION = 500; + export type { SideSliderProps }; diff --git a/src/ui/globals/types.ts b/src/ui/globals/types.ts index 52e08e727..8a16c433f 100644 --- a/src/ui/globals/types.ts +++ b/src/ui/globals/types.ts @@ -29,6 +29,7 @@ enum OperationType { SCAN_WALLET_CONNECTION = "scanWalletConnection", SCAN_SSI_BOOT_URL = "scanSSIBootUrl", SCAN_SSI_CONNECT_URL = "scanSSIConnectUrl", + OPEN_WALLET_CONNECTION_DETAIL = "openWalletConnection", } enum ToastMsgType { diff --git a/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.scss b/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.scss index bed70691d..ecab89f29 100644 --- a/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.scss +++ b/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.scss @@ -34,13 +34,14 @@ } .confirm-modal-name { - margin: 0; + margin: 0 0 1.25rem; font-size: 0.875rem; line-height: 1rem; + font-weight: 400; } .confirm-modal-id { - margin: 1.25rem 0 1.5rem; + margin: 0 0 1.5rem; padding: 1rem 1.5rem; background-color: #fff; border-radius: 1.875rem; @@ -60,6 +61,18 @@ } } + .pending-chip { + margin: 1.25rem 0 1.5rem; + border-radius: 0.25rem; + color: var(--ion-color-secondary); + background: rgba(187, 187, 187, 0.5); + + & > span { + font-size: 0.75rem; + font-weight: 400; + } + } + .confirm-connect-submit { width: 100%; --border-radius: 2rem; @@ -95,6 +108,12 @@ } } + & .page-header.md { + ion-toolbar { + max-height: none !important; + } + } + &.responsive-modal .responsive-modal-content { .confirm-modal-id { padding: 0.875rem 1.5rem; @@ -104,6 +123,15 @@ } } + .wallet-connect-fallback-logo { + width: 2.5rem; + height: 2.5rem; + } + + .confirm-modal-name-title { + font-size: 0.875rem; + } + .confirm-connect-submit { margin-bottom: 1.5rem; } diff --git a/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.test.tsx b/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.test.tsx index e5cada087..866d43743 100644 --- a/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.test.tsx +++ b/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.test.tsx @@ -36,6 +36,9 @@ const initialState = { identifiersCache: { identifiers: [...identifierFix], }, + walletConnectionsCache: { + pendingConnection: null, + }, }; const storeMocked = { @@ -68,14 +71,12 @@ describe("Confirm connect modal", () => { expect(getByTestId("wallet-connection-logo")).toBeVisible(); expect(getByText(walletConnectionsFix[0].name as string)).toBeVisible(); - expect( - getByText(walletConnectionsFix[0].selectedAid as string) - ).toBeVisible(); + expect(getByText(walletConnectionsFix[0].url || "")).toBeVisible(); const ellipsisLink = - (walletConnectionsFix[0].url as string).substring(0, 5) + + (walletConnectionsFix[0].id as string).substring(0, 5) + "..." + - (walletConnectionsFix[0].url as string).slice(-5); + (walletConnectionsFix[0].id as string).slice(-5); expect(getByText(ellipsisLink)).toBeVisible(); @@ -136,7 +137,7 @@ describe("Confirm connect modal", () => { const confirmFn = jest.fn(); const deleteFn = jest.fn(); - const { getByTestId, getByText } = render( + const { getByTestId, getByText, queryByTestId } = render( { ) ).toBeVisible(); - act(() => { - fireEvent.click(getByTestId("connection-id")); - }); - - await waitFor(() => { - expect(dispatchMock).not.toBeCalled(); - }); + expect(queryByTestId("connection-id")).toBe(null); act(() => { fireEvent.click(getByTestId("action-button")); @@ -170,4 +165,55 @@ describe("Confirm connect modal", () => { expect(deleteFn).not.toBeCalled(); }); + + test("Confirm connect modal render: connecting", async () => { + const initialState = { + stateCache: { + routes: [TabsRoutePath.IDENTIFIERS], + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + passwordIsSet: true, + }, + }, + identifiersCache: { + identifiers: [...identifierFix], + }, + walletConnectionsCache: { + pendingConnection: walletConnectionsFix[0], + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + const closeFn = jest.fn(); + const confirmFn = jest.fn(); + const deleteFn = jest.fn(); + + const { getByTestId } = render( + + + + ); + + expect(getByTestId("confirm-connect-btn").getAttribute("disabled")).toBe( + "true" + ); + expect(getByTestId("pending-chip")).toBeVisible(); + }); }); diff --git a/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.tsx b/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.tsx index dedd6ee73..fb4e81cfc 100644 --- a/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.tsx +++ b/src/ui/pages/Menu/components/ConfirmConnectModal/ConfirmConnectModal.tsx @@ -1,11 +1,18 @@ -import { IonButton, IonIcon } from "@ionic/react"; -import { copyOutline, personCircleOutline, trashOutline } from "ionicons/icons"; +import { IonButton, IonChip, IonIcon } from "@ionic/react"; +import { + copyOutline, + hourglassOutline, + personCircleOutline, + trashOutline, +} from "ionicons/icons"; import { i18n } from "../../../../../i18n"; -import { useAppDispatch } from "../../../../../store/hooks"; +import { useAppDispatch, useAppSelector } from "../../../../../store/hooks"; import { setToastMsg } from "../../../../../store/reducers/stateCache"; +import { getPendingConnection } from "../../../../../store/reducers/walletConnectionsCache"; import { OptionModal } from "../../../../components/OptionsModal"; import { ToastMsgType } from "../../../../globals/types"; import { writeToClipboard } from "../../../../utils/clipboard"; +import { ellipsisText } from "../../../../utils/formatters"; import { combineClassNames } from "../../../../utils/style"; import "./ConfirmConnectModal.scss"; import { ConfirmConnectModalProps } from "./ConfirmConnectModal.types"; @@ -19,6 +26,7 @@ const ConfirmConnectModal = ({ onDeleteConnection, }: ConfirmConnectModalProps) => { const dispatch = useAppDispatch(); + const pendingConnection = useAppSelector(getPendingConnection); const cardImg = connectionData?.iconB64 ? ( ); + const isConnecting = + !!pendingConnection && pendingConnection.id === connectionData?.id; + const dAppName = !connectionData?.name + ? ellipsisText(connectionData?.id || "", 25) + : connectionData?.name; + const buttonTitle = i18n.t( - isConnectModal - ? "menu.tab.items.connectwallet.connectionhistory.confirmconnect.connectbtn" - : "menu.tab.items.connectwallet.connectionhistory.confirmconnect.disconnectbtn" + isConnecting + ? "menu.tab.items.connectwallet.connectionhistory.confirmconnect.connectingbtn" + : isConnectModal + ? "menu.tab.items.connectwallet.connectionhistory.confirmconnect.connectbtn" + : "menu.tab.items.connectwallet.connectionhistory.confirmconnect.disconnectbtn" ); - const displayUrl = connectionData - ? (connectionData.url as string).substring(0, 5) + - "..." + - (connectionData.url as string).slice(-5) + const meerkatId = connectionData?.id + ? connectionData.id.substring(0, 5) + "..." + connectionData.id.slice(-5) : ""; const deleteConnection = () => { @@ -86,21 +100,50 @@ const ConfirmConnectModal = ({ }} > {cardImg} -

{connectionData?.name}

-

{connectionData?.selectedAid}

-
{ - if (!connectionData) return; - writeToClipboard(connectionData.url as string); - dispatch(setToastMsg(ToastMsgType.COPIED_TO_CLIPBOARD)); - }} - className="confirm-modal-id" - data-testid="connection-id" +

- {displayUrl} - -

+ {dAppName} + + {!isConnecting && ( +

+ {connectionData?.url} +

+ )} + {!isConnecting && connectionData?.name && ( +
{ + if (!connectionData?.id) return; + writeToClipboard(connectionData.id as string); + dispatch(setToastMsg(ToastMsgType.COPIED_TO_CLIPBOARD)); + }} + className="confirm-modal-id" + data-testid="connection-id" + > + {meerkatId} + +
+ )} + {isConnecting && ( + + + + {i18n.t( + "menu.tab.items.connectwallet.connectionhistory.confirmconnect.pending" + )} + + + )} ion-icon { + margin: 0; + } + } + @media screen and (min-width: 250px) and (max-width: 370px) { .connect-wallet-title { font-size: 1rem; diff --git a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx index e2f063c67..a57fa72d8 100644 --- a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx +++ b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.test.tsx @@ -13,7 +13,7 @@ import { import { OperationType, ToastMsgType } from "../../../../globals/types"; import { identifierFix } from "../../../../__fixtures__/identifierFix"; import { PeerConnection } from "../../../../../core/cardano/walletConnect/peerConnection"; -import { setPendingDAppMeerKat } from "../../../../../store/reducers/walletConnectionsCache"; +import { setPendingConnection } from "../../../../../store/reducers/walletConnectionsCache"; jest.mock("../../../../../core/agent/agent", () => ({ Agent: { @@ -68,7 +68,7 @@ const initialState = { }, walletConnectionsCache: { walletConnections: [...walletConnectionsFix], - connectedWallet: walletConnectionsFix[1].id, + connectedWallet: walletConnectionsFix[1], }, identifiersCache: { identifiers: [...identifierFix], @@ -386,9 +386,27 @@ describe("Wallet connect", () => { fireEvent.click(getByTestId("confirm-connect-btn")); }); + await waitFor(() => { + expect( + getByText( + EN_TRANSLATIONS.menu.tab.items.connectwallet + .disconnectbeforecreatealert.message + ) + ).toBeVisible(); + }); + + act(() => { + fireEvent.click( + getByText( + EN_TRANSLATIONS.menu.tab.items.connectwallet + .disconnectbeforecreatealert.confirm + ) + ); + }); + await waitFor(() => { expect(dispatchMock).toBeCalledWith( - setPendingDAppMeerKat(walletConnectionsFix[0].id) + setPendingConnection(walletConnectionsFix[0]) ); }); @@ -482,4 +500,149 @@ describe("Wallet connect", () => { ); }); }); + + test("Show connection modal after create connect to wallet", async () => { + const initialState = { + stateCache: { + routes: [TabsRoutePath.IDENTIFIERS], + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + passwordIsSet: true, + }, + currentOperation: OperationType.OPEN_WALLET_CONNECTION_DETAIL, + }, + walletConnectionsCache: { + walletConnections: [ + ...walletConnectionsFix, + { + ...walletConnectionsFix[0], + name: undefined, + url: undefined, + }, + ], + connectedWallet: null, + pendingConnection: walletConnectionsFix[0], + }, + identifiersCache: { + identifiers: [ + { + signifyName: "Test", + id: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRd", + displayName: "Professional ID", + createdAtUTC: "2023-01-01T19:23:24Z", + isPending: false, + theme: 0, + s: 4, // Sequence number, only show if s > 0 + dt: "2023-06-12T14:07:53.224866+00:00", // Last key rotation timestamp, if s > 0 + kt: 2, // Keys signing threshold (only show if kt > 1) + k: [ + // List of signing keys - array + "DCF6b0c5aVm_26_sCTgLB4An6oUxEM5pVDDLqxxXDxH-", + ], + nt: 3, // Next keys signing threshold, only show if nt > 1 + n: [ + // Next keys digests - array + "EIZ-n_hHHY5ERGTzvpXYBkB6_yBAM4RXcjQG3-JykFvF", + ], + bt: 1, // Backer threshold and backer keys below + b: ["BIe_q0F4EkYPEne6jUnSV1exxOYeGf_AMSMvegpF4XQP"], // List of backers + di: "test", // Delegated identifier prefix, don't show if "" + }, + ], + }, + biometryCache: { + enabled: false, + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + const { getByTestId, getByText, rerender } = render( + + + + + + ); + + const updatedStore = { + stateCache: { + routes: [TabsRoutePath.IDENTIFIERS], + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + passwordIsSet: true, + }, + currentOperation: OperationType.IDLE, + toastMsg: ToastMsgType.CONNECT_WALLET_SUCCESS, + }, + walletConnectionsCache: { + walletConnections: [ + ...walletConnectionsFix, + { + ...walletConnectionsFix[0], + }, + ], + connectedWallet: null, + pendingConnection: null, + }, + identifiersCache: { + identifiers: [ + { + signifyName: "Test", + id: "EN5dwY0N7RKn6OcVrK7ksIniSgPcItCuBRax2JFUpuRd", + displayName: "Professional ID", + createdAtUTC: "2023-01-01T19:23:24Z", + isPending: false, + theme: 0, + s: 4, // Sequence number, only show if s > 0 + dt: "2023-06-12T14:07:53.224866+00:00", // Last key rotation timestamp, if s > 0 + kt: 2, // Keys signing threshold (only show if kt > 1) + k: [ + // List of signing keys - array + "DCF6b0c5aVm_26_sCTgLB4An6oUxEM5pVDDLqxxXDxH-", + ], + nt: 3, // Next keys signing threshold, only show if nt > 1 + n: [ + // Next keys digests - array + "EIZ-n_hHHY5ERGTzvpXYBkB6_yBAM4RXcjQG3-JykFvF", + ], + bt: 1, // Backer threshold and backer keys below + b: ["BIe_q0F4EkYPEne6jUnSV1exxOYeGf_AMSMvegpF4XQP"], // List of backers + di: "test", // Delegated identifier prefix, don't show if "" + }, + ], + }, + biometryCache: { + enabled: false, + }, + }; + + const updateStoreMocked = { + ...mockStore(updatedStore), + dispatch: dispatchMock, + }; + + await waitFor(() => { + expect(getByTestId("connect-wallet-title")).toBeVisible(); + }); + + rerender( + + + + + + ); + + await waitFor(() => { + expect(getByTestId("connection-id")).toBeVisible(); + }); + }); }); diff --git a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx index 805abd255..1f8a0226c 100644 --- a/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx +++ b/src/ui/pages/Menu/components/ConnectWallet/ConnectWallet.tsx @@ -1,12 +1,14 @@ -import { IonCheckbox, IonItemOption } from "@ionic/react"; +import { IonCheckbox, IonChip, IonIcon, IonItemOption } from "@ionic/react"; import { forwardRef, + useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react"; import { useHistory } from "react-router-dom"; +import { hourglassOutline } from "ionicons/icons"; import { i18n } from "../../../../../i18n"; import { TabsRoutePath } from "../../../../../routes/paths"; import { useAppDispatch, useAppSelector } from "../../../../../store/hooks"; @@ -14,14 +16,17 @@ import { getIdentifiersCache } from "../../../../../store/reducers/identifiersCa import { getCurrentOperation, getStateCache, + getToastMsg, setCurrentOperation, setToastMsg, } from "../../../../../store/reducers/stateCache"; import { ConnectionData, getConnectedWallet, + getPendingConnection, getWalletConnectionsCache, - setPendingDAppMeerKat, + setConnectedWallet, + setPendingConnection, setWalletConnectionsCache, } from "../../../../../store/reducers/walletConnectionsCache"; import { Alert } from "../../../../components/Alert"; @@ -39,22 +44,27 @@ import { } from "./ConnectWallet.types"; import { Agent } from "../../../../../core/agent/agent"; import { PeerConnection } from "../../../../../core/cardano/walletConnect/peerConnection"; +import { ANIMATION_DURATION } from "../../../../components/SideSlider/SideSlider.types"; const ConnectWallet = forwardRef( (props, ref) => { const history = useHistory(); const dispatch = useAppDispatch(); + const toastMsg = useAppSelector(getToastMsg); + const pendingConnection = useAppSelector(getPendingConnection); const identifierCache = useAppSelector(getIdentifiersCache); const connections = useAppSelector(getWalletConnectionsCache); const connectedWallet = useAppSelector(getConnectedWallet); const currentOperation = useAppSelector(getCurrentOperation); const pageId = "connect-wallet-placeholder"; const stateCache = useAppSelector(getStateCache); - const actionInfo = useRef({ + const [actionInfo, setActionInfo] = useState({ type: ActionType.None, }); + const [openExistConenctedWalletAlert, setOpenExistConnectedWalletAlert] = + useState(false); const [openDeleteAlert, setOpenDeleteAlert] = useState(false); const [openConfirmConnectModal, setOpenConfirmConnectModal] = useState(false); @@ -65,14 +75,18 @@ const ConnectWallet = forwardRef( const [verifyPasscodeIsOpen, setVerifyPasscodeIsOpen] = useState(false); const displayConnection = useMemo((): CardItem[] => { - return connections.map((connection) => ({ - id: connection.id, - title: connection.name as string, - url: connection.url, - subtitle: connection.url, - image: connection.iconB64, - data: connection, - })); + return connections.map((connection) => { + const dAppName = connection.name ? connection.name : connection.id; + + return { + id: connection.id, + title: dAppName, + url: connection.url, + subtitle: connection.url, + image: connection.iconB64, + data: connection, + }; + }); }, [connections]); useImperativeHandle(ref, () => ({ @@ -91,26 +105,27 @@ const ConnectWallet = forwardRef( }; const handleOpenDeleteAlert = (data: ConnectionData) => { - actionInfo.current = { + setActionInfo({ type: ActionType.Delete, data, - }; + }); setOpenDeleteAlert(true); }; const handleOpenConfirmConnectModal = (data: ConnectionData) => { - actionInfo.current = { + setActionInfo({ type: ActionType.Connect, data, - }; + }); setOpenConfirmConnectModal(true); }; const closeDeleteAlert = () => { - actionInfo.current = { + setActionInfo({ type: ActionType.None, - }; + }); + setOpenDeleteAlert(false); }; @@ -120,9 +135,9 @@ const ConnectWallet = forwardRef( }; const handleDeleteConnection = async (data: ConnectionData) => { - actionInfo.current = { + setActionInfo({ type: ActionType.None, - }; + }); await Agent.agent.peerConnectionMetadataStorage.deletePeerConnectionMetadataRecord( data.id ); @@ -132,32 +147,47 @@ const ConnectWallet = forwardRef( connections.filter((connection) => connection.id !== data.id) ) ); + + if (data.id === pendingConnection?.id) { + dispatch(setPendingConnection(null)); + } + dispatch(setToastMsg(ToastMsgType.WALLET_CONNECTION_DELETED)); }; - const handleConnectWallet = () => { + const disconnectWallet = () => { + if (!connectedWallet) return; + PeerConnection.peerConnection.disconnectDApp(connectedWallet?.id); + dispatch(setConnectedWallet(null)); + }; + + const toggleConnected = () => { if (identifierCache.length === 0) { setOpenIdentifierMissingAlert(true); return; } - if (!actionInfo.current.data) return; - const isConnectedItem = actionInfo.current.data.id === connectedWallet; + + if (!actionInfo.data) return; + const isConnectedItem = actionInfo.data.id === connectedWallet?.id; if (isConnectedItem) { - PeerConnection.peerConnection.disconnectDApp(connectedWallet); - } else { - dispatch(setPendingDAppMeerKat(actionInfo.current.data.id)); + disconnectWallet(); + return; } + + if (connectedWallet) { + setOpenExistConnectedWalletAlert(true); + return; + } + + dispatch(setPendingConnection(actionInfo.data)); }; const handleAfterVerify = () => { setVerifyPasscodeIsOpen(false); setVerifyPasswordIsOpen(false); - if ( - actionInfo.current.type === ActionType.Delete && - actionInfo.current.data - ) { - handleDeleteConnection(actionInfo.current.data); + if (actionInfo.type === ActionType.Delete && actionInfo.data) { + handleDeleteConnection(actionInfo.data); } }; @@ -167,9 +197,34 @@ const ConnectWallet = forwardRef( return; } + if (connectedWallet) { + setActionInfo({ + type: ActionType.Add, + }); + setOpenExistConnectedWalletAlert(true); + return; + } + dispatch(setCurrentOperation(OperationType.SCAN_WALLET_CONNECTION)); }; + const handleCloseExistConnectedWallet = () => { + setOpenExistConnectedWalletAlert(false); + setActionInfo({ + type: ActionType.None, + }); + }; + + const handleContinueScanQRWithExistedConnection = () => { + disconnectWallet(); + if (actionInfo.type === ActionType.Connect && actionInfo.data) { + dispatch(setPendingConnection(actionInfo.data)); + } else { + dispatch(setCurrentOperation(OperationType.SCAN_WALLET_CONNECTION)); + } + handleCloseExistConnectedWallet(); + }; + const closeIdentifierMissingAlert = () => { setOpenIdentifierMissingAlert(false); }; @@ -182,6 +237,35 @@ const ConnectWallet = forwardRef( history.push(TabsRoutePath.IDENTIFIERS); }; + // NOTE: Reload connection data after connect success + useEffect(() => { + if ( + toastMsg === ToastMsgType.CONNECT_WALLET_SUCCESS && + !pendingConnection && + connectedWallet && + openConfirmConnectModal + ) { + setActionInfo({ + type: ActionType.Connect, + data: connectedWallet, + }); + } + }, [connectedWallet, toastMsg, pendingConnection]); + + useEffect(() => { + if (!pendingConnection) return; + + if ( + OperationType.OPEN_WALLET_CONNECTION_DETAIL === currentOperation && + pendingConnection + ) { + dispatch(setCurrentOperation(OperationType.IDLE)); + setTimeout(() => { + handleOpenConfirmConnectModal(pendingConnection); + }, ANIMATION_DURATION); + } + }, [currentOperation, pendingConnection]); + return ( <>
@@ -209,7 +293,18 @@ const ConnectWallet = forwardRef( ); }} onRenderEndSlot={(data) => { - if (data.id !== connectedWallet) return null; + if (data.id === pendingConnection?.id) { + return ( + + + + ); + } + + if (data.id !== connectedWallet?.id) return null; return ( ( )}
setOpenConfirmConnectModal(false)} - onConfirm={handleConnectWallet} - connectionData={actionInfo.current.data} + onConfirm={toggleConnected} + connectionData={actionInfo.data} onDeleteConnection={handleOpenDeleteAlert} /> ( actionCancel={closeDeleteAlert} actionDismiss={closeDeleteAlert} /> + { identifiers: [...identifierFix], }, walletConnectionsCache: { - pendingDAppMeerKat: "pending-meerkat", + pendingConnection: "pending-meerkat", + walletConnections: [], }, }; diff --git a/src/ui/pages/SidePage/SidePage.tsx b/src/ui/pages/SidePage/SidePage.tsx index 6c4b9c1d0..9a1ee179e 100644 --- a/src/ui/pages/SidePage/SidePage.tsx +++ b/src/ui/pages/SidePage/SidePage.tsx @@ -6,7 +6,7 @@ import { setPauseQueueIncomingRequest, } from "../../../store/reducers/stateCache"; import { useAppDispatch, useAppSelector } from "../../../store/hooks"; -import { getPendingDAppMeerkat } from "../../../store/reducers/walletConnectionsCache"; +import { getPendingConnection } from "../../../store/reducers/walletConnectionsCache"; import { IncomingRequest } from "./components/IncomingRequest"; import { WalletConnect } from "./components/WalletConnect"; @@ -16,12 +16,12 @@ const SidePage = () => { const pauseIncommingRequestByConnection = useRef(false); const queueIncomingRequest = useAppSelector(getQueueIncomingRequest); - const pendingDAppMeerkat = useAppSelector(getPendingDAppMeerkat); + const pendingConnection = useAppSelector(getPendingConnection); const stateCache = useAppSelector(getStateCache); const canOpenIncomingRequest = queueIncomingRequest.queues.length > 0 && !queueIncomingRequest.isPaused; - const canOpenPendingWalletConnection = !!pendingDAppMeerkat; + const canOpenPendingWalletConnection = !!pendingConnection; useEffect(() => { if (canOpenIncomingRequest || !stateCache.authentication.loggedIn) return; diff --git a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx index a9eee7588..2ec4b2d8d 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.tsx @@ -13,6 +13,7 @@ import { IncomingRequestType, } from "../../../../../store/reducers/stateCache/stateCache.types"; import { getConnectedWallet } from "../../../../../store/reducers/walletConnectionsCache"; +import { ANIMATION_DURATION } from "../../../../components/SideSlider/SideSlider.types"; const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { const pageId = "incoming-request"; @@ -38,7 +39,7 @@ const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { if ( incomingRequest.type === IncomingRequestType.PEER_CONNECT_SIGN && (!connectedWallet || - connectedWallet !== incomingRequest.peerConnection?.id) + connectedWallet.id !== incomingRequest.peerConnection?.id) ) { handleReset(); } diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.test.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.test.tsx index 72f07fc0d..d3dd39a6f 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.test.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.test.tsx @@ -14,6 +14,7 @@ import { setToastMsg } from "../../../../../store/reducers/stateCache"; import { ToastMsgType } from "../../../../globals/types"; import { WalletConnect } from "./WalletConnect"; import { setWalletConnectionsCache } from "../../../../../store/reducers/walletConnectionsCache"; +import { walletConnectionsFix } from "../../../../__fixtures__/walletConnectionsFix"; setupIonicReact(); mockIonicReact(); @@ -260,7 +261,7 @@ describe("Wallet Connect Request", () => { }, walletConnectionsCache: { walletConnections: [], - pendingDAppMeerKat: "pending-meerkat", + pendingConnection: walletConnectionsFix[0], }, identifiersCache: { identifiers: [...identifierFix], diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx index 4a0e848de..f7c2efc0b 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnect.tsx @@ -1,24 +1,16 @@ -import { useEffect, useState } from "react"; -import { useAppDispatch, useAppSelector } from "../../../../../store/hooks"; -import { - getPendingDAppMeerkat, - setPendingDAppMeerKat, -} from "../../../../../store/reducers/walletConnectionsCache"; +import { useState } from "react"; +import { useAppSelector } from "../../../../../store/hooks"; +import { getPendingConnection } from "../../../../../store/reducers/walletConnectionsCache"; +import { SideSlider } from "../../../../components/SideSlider"; +import { SidePageContentProps } from "../../SidePage.types"; import { WalletConnectStageOne } from "./WalletConnectStageOne"; import { WalletConnectStageTwo } from "./WalletConnectStageTwo"; -import { SidePageContentProps } from "../../SidePage.types"; -import { SideSlider } from "../../../../components/SideSlider"; const WalletConnect = ({ setOpenPage }: SidePageContentProps) => { - const dispatch = useAppDispatch(); - const pendingDAppMeerkat = useAppSelector(getPendingDAppMeerkat); + const pendingConnection = useAppSelector(getPendingConnection); const [requestStage, setRequestStage] = useState(0); const [hiddenStageOne, setHiddenStageOne] = useState(false); - useEffect(() => { - setTimeout(() => setOpenPage(!!pendingDAppMeerkat), 10); - }, [pendingDAppMeerkat]); - const changeToStageTwo = () => { setTimeout(() => setHiddenStageOne(true), 400); setRequestStage(1); @@ -31,27 +23,23 @@ const WalletConnect = ({ setOpenPage }: SidePageContentProps) => { const handleCloseWalletConnect = () => { setOpenPage(false); - - setTimeout(() => { - dispatch(setPendingDAppMeerKat(null)); - }, 500); }; - if (!pendingDAppMeerkat) return null; + if (!pendingConnection) return null; return (
{!hiddenStageOne && ( )} diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageOne.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageOne.tsx index 797ed1880..ab272062e 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageOne.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageOne.tsx @@ -9,6 +9,9 @@ import { ResponsivePageLayout } from "../../../../components/layout/ResponsivePa import { combineClassNames } from "../../../../utils/style"; import "./WalletConnect.scss"; import { WalletConnectStageOneProps } from "./WalletConnect.types"; +import { useAppDispatch } from "../../../../../store/hooks"; +import { setPendingConnection } from "../../../../../store/reducers/walletConnectionsCache"; +import { ANIMATION_DURATION } from "../../../../components/SideSlider/SideSlider.types"; const WalletConnectStageOne = ({ isOpen, @@ -16,6 +19,7 @@ const WalletConnectStageOne = ({ onClose, onAccept, }: WalletConnectStageOneProps) => { + const dispatch = useAppDispatch(); const [openDeclineAlert, setOpenDeclineAlert] = useState(false); const classes = combineClassNames(className, { @@ -29,6 +33,10 @@ const WalletConnectStageOne = ({ const handleClose = () => { onClose(); + + setTimeout(() => { + dispatch(setPendingConnection(null)); + }, ANIMATION_DURATION); }; const handleAccept = () => { diff --git a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx index 5c8d1573d..9a6d1f16d 100644 --- a/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx +++ b/src/ui/pages/SidePage/components/WalletConnect/WalletConnectStageTwo.tsx @@ -5,21 +5,24 @@ import { IdentifierShortDetails } from "../../../../../core/agent/services/ident import { i18n } from "../../../../../i18n"; import { useAppSelector } from "../../../../../store/hooks"; import { getIdentifiersCache } from "../../../../../store/reducers/identifiersCache"; -import { setToastMsg } from "../../../../../store/reducers/stateCache"; +import { + setCurrentOperation, + setToastMsg, +} from "../../../../../store/reducers/stateCache"; import { CardItem, CardList } from "../../../../components/CardList"; import { PageFooter } from "../../../../components/PageFooter"; import { PageHeader } from "../../../../components/PageHeader"; import { ResponsivePageLayout } from "../../../../components/layout/ResponsivePageLayout"; -import { ToastMsgType } from "../../../../globals/types"; +import { OperationType, ToastMsgType } from "../../../../globals/types"; import { combineClassNames } from "../../../../utils/style"; import "./WalletConnect.scss"; import { WalletConnectStageTwoProps } from "./WalletConnect.types"; import { PeerConnection } from "../../../../../core/cardano/walletConnect/peerConnection"; -import { Agent } from "../../../../../core/agent/agent"; import { getWalletConnectionsCache, setWalletConnectionsCache, } from "../../../../../store/reducers/walletConnectionsCache"; +import KeriLogo from "../../../../assets/images/KeriGeneric.jpg"; const WalletConnectStageTwo = ({ isOpen, @@ -38,7 +41,7 @@ const WalletConnectStageTwo = ({ (identifier, index): CardItem => ({ id: index, title: identifier.displayName, - image: "", + image: KeriLogo, data: identifier, }) ); @@ -60,7 +63,10 @@ const WalletConnectStageTwo = ({ const existingConnection = existingConnections.find( (connection) => connection.id === pendingDAppMeerkat ); - if (!existingConnection) { + if (existingConnection) { + existingConnection.selectedAid = selectedIdentifier.id; + dispatch(setWalletConnectionsCache([...existingConnections])); + } else { // Insert a new connection if needed dispatch( setWalletConnectionsCache([ @@ -69,6 +75,10 @@ const WalletConnectStageTwo = ({ ]) ); } + + dispatch( + setCurrentOperation(OperationType.OPEN_WALLET_CONNECTION_DETAIL) + ); } onClose(); } catch (e) { diff --git a/src/ui/utils/formatters.test.ts b/src/ui/utils/formatters.test.ts index d8a0bbbb7..ecbbb2c4e 100644 --- a/src/ui/utils/formatters.test.ts +++ b/src/ui/utils/formatters.test.ts @@ -1,8 +1,12 @@ -import { formatCurrencyUSD } from "./formatters"; +import { ellipsisText, formatCurrencyUSD } from "./formatters"; describe("Utils", () => { test("formatCurrencyUSD", () => { const balance = 1012.0; expect(formatCurrencyUSD(balance)).toBe("$1,012.00"); }); + + test("ellipsisText", () => { + expect(ellipsisText("text text text", 3)).toBe("tex..."); + }); }); diff --git a/src/ui/utils/formatters.ts b/src/ui/utils/formatters.ts index 59b0647c3..010de04be 100644 --- a/src/ui/utils/formatters.ts +++ b/src/ui/utils/formatters.ts @@ -34,10 +34,15 @@ const formatCurrencyUSD = (amount: number) => { return currencyFormat.format(amount); }; +const ellipsisText = (raw: string, length: number, suffix = "...") => { + return raw.substring(0, length || raw.length) + suffix; +}; + export { formatShortDate, formatLongDate, formatShortTime, formatTimeToSec, formatCurrencyUSD, + ellipsisText, }; From bbe3895840c50f2990b7ab03efdddcbf478a92e1 Mon Sep 17 00:00:00 2001 From: Bao Hoang <142210850+bao-sotatek@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:35:26 +0700 Subject: [PATCH 17/28] feat(cred-serv): requestDisclosure receives dynamic attributes fields (#513) --- services/credential-issuance-server/src/agent.ts | 5 +++-- .../credential-issuance-server/src/apis/credential.api.ts | 4 ++-- .../src/modules/signify/signifyApi.ts | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/services/credential-issuance-server/src/agent.ts b/services/credential-issuance-server/src/agent.ts index 4201443a9..82f0b1ebe 100644 --- a/services/credential-issuance-server/src/agent.ts +++ b/services/credential-issuance-server/src/agent.ts @@ -48,11 +48,12 @@ class Agent { ); } - async requestDisclosure(schemaSaid, aid) { + async requestDisclosure(schemaSaid, aid, attributes) { return this.signifyApi.requestDisclosure( Agent.ISSUER_AID_NAME, schemaSaid, - aid + aid, + attributes ); } diff --git a/services/credential-issuance-server/src/apis/credential.api.ts b/services/credential-issuance-server/src/apis/credential.api.ts index 28f882548..8a7c22675 100644 --- a/services/credential-issuance-server/src/apis/credential.api.ts +++ b/services/credential-issuance-server/src/apis/credential.api.ts @@ -24,8 +24,8 @@ async function issueAcdcCredential(req: Request, res: Response): Promise { } async function requestDisclosure(req: Request, res: Response): Promise { - const { schemaSaid, aid } = req.body; - await Agent.agent.requestDisclosure(schemaSaid, aid); + const { schemaSaid, aid, attributes } = req.body; + await Agent.agent.requestDisclosure(schemaSaid, aid, attributes); const response: ResponseData = { statusCode: 200, success: true, diff --git a/services/credential-issuance-server/src/modules/signify/signifyApi.ts b/services/credential-issuance-server/src/modules/signify/signifyApi.ts index 53964a192..c3d7a5df8 100644 --- a/services/credential-issuance-server/src/modules/signify/signifyApi.ts +++ b/services/credential-issuance-server/src/modules/signify/signifyApi.ts @@ -141,12 +141,14 @@ export class SignifyApi { async requestDisclosure( senderName: string, schemaSaid: string, - recipient: string + recipient: string, + attributes: { [key: string]: string } ) { const [apply, sigs] = await this.signifyClient.ipex().apply({ senderName, recipient, schema: schemaSaid, + attributes, }); await this.signifyClient .ipex() From e3822cefaeaebe828155f8667d933148b30f0434 Mon Sep 17 00:00:00 2001 From: Patrick Nguyen Date: Wed, 12 Jun 2024 16:36:00 +0700 Subject: [PATCH 18/28] refactor: clean incoming requests props (#514) * refactor: change incomingRequestProps type * refactor: clean incoming requests props * refactor: clean incoming requests props * refactor: clean incoming requests props * refactor: clean incoming requests props --- .../reducers/stateCache/stateCache.test.ts | 18 ++++++++ .../reducers/stateCache/stateCache.types.ts | 33 ++++++++++---- .../components/AppWrapper/AppWrapper.test.tsx | 1 - src/ui/components/AppWrapper/AppWrapper.tsx | 31 +++++++------ .../components/IdentifierStage1.tsx | 13 ++++-- .../IncomingRequest/IncomingRequest.test.tsx | 22 +++++++-- .../IncomingRequest/IncomingRequest.tsx | 26 +++++++---- .../IncomingRequest/IncomingRequest.types.ts | 10 +++-- .../components/ConnectionRequest.tsx | 3 +- .../components/CredentialRequest.tsx | 3 +- .../components/MultiSigRequest.tsx | 4 +- .../components/RequestComponent.test.tsx | 45 ++++++++++--------- .../components/RequestComponent.tsx | 6 +-- .../components/SignRequest.tsx | 21 +++++---- 14 files changed, 153 insertions(+), 83 deletions(-) diff --git a/src/store/reducers/stateCache/stateCache.test.ts b/src/store/reducers/stateCache/stateCache.test.ts index 7d6286812..8238648e1 100644 --- a/src/store/reducers/stateCache/stateCache.test.ts +++ b/src/store/reducers/stateCache/stateCache.test.ts @@ -100,6 +100,8 @@ describe("State Cache", () => { const connectionCredentialRequestProps: IncomingRequestProps = { id: "123", type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED, + logo: "logo", + label: "label", }; const action = setQueueIncomingRequest(connectionCredentialRequestProps); const nextState = stateCacheSlice.reducer(initialState, action); @@ -116,16 +118,22 @@ describe("State Cache", () => { { id: "123", type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED, + logo: "logo1", + label: "label1", }, ]; const batchIncomingRequestProps: IncomingRequestProps[] = [ { id: "456", type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED, + logo: "logo2", + label: "label2", }, { id: "789", type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED, + logo: "logo3", + label: "label3", }, ]; const action = enqueueIncomingRequest(batchIncomingRequestProps); @@ -144,10 +152,14 @@ describe("State Cache", () => { { id: "123", type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED, + logo: "logo1", + label: "label1", }, { id: "456", type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED, + logo: "logo2", + label: "label2", }, ]; const action = dequeueCredentialRequest(); @@ -168,6 +180,8 @@ describe("State Cache", () => { const connectionCredentialRequestProps: IncomingRequestProps = { id: "123", type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED, + logo: "logo", + label: "label", }; const action2 = setQueueIncomingRequest(connectionCredentialRequestProps); const nextState2 = stateCacheSlice.reducer(nextState1, action2); @@ -185,10 +199,14 @@ describe("State Cache", () => { { id: "123", type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED, + logo: "logo1", + label: "label1", }, { id: "", type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED, + logo: "logo2", + label: "label2", }, ]; const action = dequeueCredentialRequest(); diff --git a/src/store/reducers/stateCache/stateCache.types.ts b/src/store/reducers/stateCache/stateCache.types.ts index c4a42981d..7a15a075b 100644 --- a/src/store/reducers/stateCache/stateCache.types.ts +++ b/src/store/reducers/stateCache/stateCache.types.ts @@ -28,16 +28,30 @@ enum IncomingRequestType { PEER_CONNECT_SIGN = "peer-connect-sign", } -interface IncomingRequestProps { +type MultiSigRequest = { id: string; - type?: IncomingRequestType; - logo?: string; - label?: string; - event?: KeriaNotification; - multisigIcpDetails?: MultiSigIcpRequestDetails; - signTransaction?: PeerConnectSigningEvent; - peerConnection?: ConnectionData; -} + event: KeriaNotification; + type: IncomingRequestType.MULTI_SIG_REQUEST_INCOMING; + multisigIcpDetails: MultiSigIcpRequestDetails; +}; + +type PeerConnectSigningEventRequest = { + type: IncomingRequestType.PEER_CONNECT_SIGN; + signTransaction: PeerConnectSigningEvent; + peerConnection: ConnectionData; +}; + +type KeriaNotificationRequest = { + id: string; + type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED; + logo: string; + label: string; +}; + +type IncomingRequestProps = + | KeriaNotificationRequest + | MultiSigRequest + | PeerConnectSigningEventRequest; interface QueueProps { isPaused: boolean; @@ -63,4 +77,5 @@ export type { StateCacheProps, IncomingRequestProps, QueueProps, + PeerConnectSigningEventRequest, }; diff --git a/src/ui/components/AppWrapper/AppWrapper.test.tsx b/src/ui/components/AppWrapper/AppWrapper.test.tsx index e6f486d1c..05756fa1c 100644 --- a/src/ui/components/AppWrapper/AppWrapper.test.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.test.tsx @@ -356,7 +356,6 @@ describe("AppWrapper handler", () => { ); expect(dispatch).toBeCalledWith( setQueueIncomingRequest({ - id: "peer-connect-signing", signTransaction: peerSignRequestEventMock, peerConnection: peerConnectionMock, type: IncomingRequestType.PEER_CONNECT_SIGN, diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index 959688426..ce49e93ef 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -159,7 +159,6 @@ const peerConnectRequestSignChangeHandler = async ( ); dispatch( setQueueIncomingRequest({ - id: "peer-connect-signing", signTransaction: event, peerConnection, type: IncomingRequestType.PEER_CONNECT_SIGN, @@ -186,7 +185,7 @@ const peerConnectedChangeHandler = async ( const peerDisconnectedChangeHandler = async ( event: PeerDisconnectedEvent, - connectedMeerKat: string, + connectedMeerKat: string | null, dispatch: ReturnType ) => { if (connectedMeerKat === event.payload.dAppAddress) { @@ -264,7 +263,22 @@ const AppWrapper = (props: { children: ReactNode }) => { } else { dispatch(setPauseQueueIncomingRequest(true)); } - }, [isOnline, authentication.loggedIn, dispatch]); + }, [isOnline, authentication.loggedIn, isMessagesHandled, dispatch]); + + useEffect(() => { + PeerConnection.peerConnection.onPeerDisconnectedStateChanged( + async (event) => { + if (!connectedWallet) { + return; + } + return peerDisconnectedChangeHandler( + event, + connectedWallet.id, + dispatch + ); + } + ); + }, [connectedWallet, dispatch]); const checkKeyStore = async (key: string) => { try { @@ -413,17 +427,6 @@ const AppWrapper = (props: { children: ReactNode }) => { PeerConnection.peerConnection.onPeerConnectedStateChanged(async (event) => { return peerConnectedChangeHandler(event, dispatch); }); - PeerConnection.peerConnection.onPeerDisconnectedStateChanged( - async (event) => { - if (connectedWallet) { - return peerDisconnectedChangeHandler( - event, - connectedWallet.id, - dispatch - ); - } - } - ); PeerConnection.peerConnection.onPeerConnectionBrokenStateChanged( async (event) => { setIsAlertPeerBrokenOpen(true); diff --git a/src/ui/components/CreateIdentifier/components/IdentifierStage1.tsx b/src/ui/components/CreateIdentifier/components/IdentifierStage1.tsx index d8c09c1da..a61a2eefe 100644 --- a/src/ui/components/CreateIdentifier/components/IdentifierStage1.tsx +++ b/src/ui/components/CreateIdentifier/components/IdentifierStage1.tsx @@ -17,6 +17,7 @@ import { TabsRoutePath } from "../../navigation/TabsMenu"; import { OperationType } from "../../../globals/types"; import { getMultiSigGroupCache } from "../../../../store/reducers/identifiersCache"; import { ConnectionShortDetails } from "../../../pages/Connections/Connections.types"; +import { IncomingRequestType } from "../../../../store/reducers/stateCache/stateCache.types"; const IdentifierStage1 = ({ state, @@ -48,10 +49,10 @@ const IdentifierStage1 = ({ >([]); const incomingRequest = useMemo(() => { return !queueIncomingRequest.isProcessing - ? { id: "" } + ? undefined : queueIncomingRequest.queues.length > 0 ? queueIncomingRequest.queues[0] - : { id: "" }; + : undefined; }, [queueIncomingRequest]); useEffect(() => { @@ -87,8 +88,12 @@ const IdentifierStage1 = ({ }, [groupMetadata, currentOperation, groupId, multiSigGroupCache]); useEffect(() => { - incomingRequest.multisigIcpDetails?.ourIdentifier.groupMetadata?.groupId === - groupId && handleDone(); + if ( + incomingRequest?.type === IncomingRequestType.MULTI_SIG_REQUEST_INCOMING + ) { + incomingRequest.multisigIcpDetails.ourIdentifier.groupMetadata + ?.groupId === groupId && handleDone(); + } }, [groupMetadata, incomingRequest]); const handleDone = () => { diff --git a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx index 699c1786a..8cc690714 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.test.tsx @@ -5,7 +5,10 @@ import { EventEmitter } from "events"; import { Provider } from "react-redux"; import { store } from "../../../../../store"; import { IncomingRequest } from "./IncomingRequest"; -import { IncomingRequestType } from "../../../../../store/reducers/stateCache/stateCache.types"; +import { + IncomingRequestProps, + IncomingRequestType, +} from "../../../../../store/reducers/stateCache/stateCache.types"; import { connectionsFix } from "../../../../__fixtures__/connectionsFix"; import EN_TRANSLATIONS from "../../../../../locales/en/en.json"; import { setQueueIncomingRequest } from "../../../../../store/reducers/stateCache"; @@ -134,6 +137,11 @@ describe("Multi-Sig request", () => { const requestDetails = { id: "abc123456", type: IncomingRequestType.MULTI_SIG_REQUEST_INCOMING, + event: { + id: "event-id", + createdAt: new Date(), + a: { d: "d" }, + }, multisigIcpDetails: { ourIdentifier: filteredIdentifierFix[0], sender: connectionsFix[3], @@ -149,7 +157,9 @@ describe("Multi-Sig request", () => { }); test("It receives incoming Multi-Sig request and render content in MultiSigRequest", async () => { - store.dispatch(setQueueIncomingRequest(requestDetails)); + store.dispatch( + setQueueIncomingRequest(requestDetails as IncomingRequestProps) + ); const { getByText } = render( @@ -178,7 +188,9 @@ describe("Multi-Sig request", () => { }); test("Selecting Cancel will open the Alert pop-up", async () => { - store.dispatch(setQueueIncomingRequest(requestDetails)); + store.dispatch( + setQueueIncomingRequest(requestDetails as IncomingRequestProps) + ); const { getByText } = render( { }); test("Selecting Accept will open the Alert pop-up", async () => { - store.dispatch(setQueueIncomingRequest(requestDetails)); + store.dispatch( + setQueueIncomingRequest(requestDetails as IncomingRequestProps) + ); const { getByText } = render( { !queueIncomingRequest.isProcessing || !queueIncomingRequest.queues.length ) { - return { id: "" }; + return; } else { return queueIncomingRequest.queues[0]; } @@ -36,6 +36,9 @@ const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { const [blur, setBlur] = useState(false); useEffect(() => { + if (!incomingRequest) { + return; + } if ( incomingRequest.type === IncomingRequestType.PEER_CONNECT_SIGN && (!connectedWallet || @@ -43,7 +46,7 @@ const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { ) { handleReset(); } - if (incomingRequest.id.length > 0) { + if (incomingRequest) { setRequestData(incomingRequest); setOpenPage(true); } @@ -68,6 +71,9 @@ const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { }; const handleCancel = async () => { + if (!incomingRequest) { + return handleReset(); + } if ( incomingRequest.type === IncomingRequestType.CREDENTIAL_OFFER_RECEIVED ) { @@ -87,6 +93,9 @@ const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { }; const handleAccept = async () => { + if (!incomingRequest) { + return handleReset(); + } setInitiateAnimation(true); if ( incomingRequest.type === IncomingRequestType.CREDENTIAL_OFFER_RECEIVED @@ -101,6 +110,9 @@ const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { }; const handleIgnore = async () => { + if (!incomingRequest) { + return handleReset(); + } if ( incomingRequest.type === IncomingRequestType.MULTI_SIG_REQUEST_INCOMING ) { @@ -111,22 +123,20 @@ const IncomingRequest = ({ open, setOpenPage }: SidePageContentProps) => { handleReset(); }; - const defaultRequestData: IncomingRequestProps = { - id: "", - }; - + if (!requestData) { + return null; + } return ( ); }; diff --git a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.types.ts b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.types.ts index c9a2992c0..bdd5aecf9 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.types.ts +++ b/src/ui/pages/SidePage/components/IncomingRequest/IncomingRequest.types.ts @@ -1,16 +1,18 @@ -import { IncomingRequestProps } from "../../../../../store/reducers/stateCache/stateCache.types"; +import { + IncomingRequestProps, + IncomingRequestType, +} from "../../../../../store/reducers/stateCache/stateCache.types"; -interface RequestProps { +interface RequestProps { pageId: string; activeStatus?: boolean; blur?: boolean; setBlur?: (value: boolean) => void; - requestData: IncomingRequestProps; + requestData: IncomingRequestProps & { type: T }; initiateAnimation: boolean; handleAccept: () => void; handleCancel: () => void; handleIgnore?: () => void; - incomingRequestType?: string; } export type { RequestProps }; diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/ConnectionRequest.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/ConnectionRequest.tsx index b746e5f39..c754bd233 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/ConnectionRequest.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/ConnectionRequest.tsx @@ -11,6 +11,7 @@ import { ResponsivePageLayout } from "../../../../../components/layout/Responsiv import { i18n } from "../../../../../../i18n"; import { RequestType } from "../../../../../globals/types"; import { PageFooter } from "../../../../../components/PageFooter"; +import { IncomingRequestType } from "../../../../../../store/reducers/stateCache/stateCache.types"; const ConnectionRequest = ({ pageId, @@ -19,7 +20,7 @@ const ConnectionRequest = ({ initiateAnimation, handleAccept, handleCancel, -}: RequestProps) => { +}: RequestProps) => { return ( { +}: RequestProps) => { const fallbackLogo = KeriLogo; return ( diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/MultiSigRequest.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/MultiSigRequest.tsx index 298d92f02..fa458efbe 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/MultiSigRequest.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/MultiSigRequest.tsx @@ -31,6 +31,7 @@ import { i18n } from "../../../../../../i18n"; import { PageFooter } from "../../../../../components/PageFooter"; import "./MultiSigRequest.scss"; import { CreateIdentifierResult } from "../../../../../../core/agent/agent.types"; +import { IncomingRequestType } from "../../../../../../store/reducers/stateCache/stateCache.types"; const MultiSigRequest = ({ blur, @@ -40,12 +41,11 @@ const MultiSigRequest = ({ requestData, handleAccept, handleCancel, -}: RequestProps) => { +}: RequestProps) => { const dispatch = useAppDispatch(); const identifiersData = useAppSelector(getIdentifiersCache); const [alertAcceptIsOpen, setAlertAcceptIsOpen] = useState(false); const [alertDeclineIsOpen, setAlertDeclineIsOpen] = useState(false); - const actionAccept = async () => { setAlertAcceptIsOpen(false); setBlur && setBlur(true); diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.test.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.test.tsx index 3ebfbae68..0f23375bb 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.test.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.test.tsx @@ -5,7 +5,10 @@ import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; import EN_TRANSLATIONS from "../../../../../../locales/en/en.json"; import { store } from "../../../../../../store"; -import { IncomingRequestType } from "../../../../../../store/reducers/stateCache/stateCache.types"; +import { + IncomingRequestProps, + IncomingRequestType, +} from "../../../../../../store/reducers/stateCache/stateCache.types"; import { connectionsFix } from "../../../../../__fixtures__/connectionsFix"; import { filteredIdentifierFix } from "../../../../../__fixtures__/filteredIdentifierFix"; import { @@ -44,6 +47,11 @@ describe("Multi-Sig request", () => { const requestData = { id: "abc123456", type: IncomingRequestType.MULTI_SIG_REQUEST_INCOMING, + event: { + id: "event-id", + createdAt: new Date(), + a: { d: "d" }, + }, multisigIcpDetails: { ourIdentifier: filteredIdentifierFix[0], sender: connectionsFix[3], @@ -51,6 +59,12 @@ describe("Multi-Sig request", () => { threshold: 1, }, }; + const credentialRequestData = { + id: "id", + type: IncomingRequestType.CREDENTIAL_OFFER_RECEIVED, + logo: "logo", + label: "label", + }; const initiateAnimation = false; const handleAccept = jest.fn(); const handleCancel = jest.fn(); @@ -64,12 +78,11 @@ describe("Multi-Sig request", () => { activeStatus={activeStatus} blur={blur} setBlur={setBlur} - requestData={requestData} + requestData={credentialRequestData as any} initiateAnimation={initiateAnimation} handleAccept={handleAccept} handleCancel={handleCancel} handleIgnore={handleIgnore} - incomingRequestType={IncomingRequestType.CREDENTIAL_OFFER_RECEIVED} /> ); @@ -80,11 +93,6 @@ describe("Multi-Sig request", () => { }); test("Display fallback image when provider logo is empty: CREDENTIAL_OFFER_RECEIVED", async () => { - const testData = { - ...requestData, - logo: "", - }; - const { getByTestId } = render( { activeStatus={activeStatus} blur={blur} setBlur={setBlur} - requestData={testData} + requestData={credentialRequestData as any} initiateAnimation={initiateAnimation} handleAccept={handleAccept} handleCancel={handleCancel} handleIgnore={handleIgnore} - incomingRequestType={IncomingRequestType.CREDENTIAL_OFFER_RECEIVED} /> ); @@ -117,12 +124,11 @@ describe("Multi-Sig request", () => { activeStatus={activeStatus} blur={blur} setBlur={setBlur} - requestData={requestData} + requestData={requestData as IncomingRequestProps} initiateAnimation={initiateAnimation} handleAccept={handleAccept} handleCancel={handleCancel} handleIgnore={handleIgnore} - incomingRequestType={IncomingRequestType.MULTI_SIG_REQUEST_INCOMING} /> ); @@ -163,12 +169,11 @@ describe("Multi-Sig request", () => { activeStatus={activeStatus} blur={blur} setBlur={setBlur} - requestData={requestData} + requestData={requestData as IncomingRequestProps} initiateAnimation={initiateAnimation} handleAccept={handleAccept} handleCancel={handleCancel} handleIgnore={handleIgnore} - incomingRequestType={IncomingRequestType.MULTI_SIG_REQUEST_INCOMING} /> ); @@ -220,12 +225,11 @@ describe("Sign request", () => { activeStatus={activeStatus} blur={blur} setBlur={setBlur} - requestData={requestData} + requestData={requestData as IncomingRequestProps} initiateAnimation={initiateAnimation} handleAccept={handleAccept} handleCancel={handleCancel} handleIgnore={handleIgnore} - incomingRequestType={IncomingRequestType.PEER_CONNECT_SIGN} /> ); @@ -252,12 +256,11 @@ describe("Sign request", () => { activeStatus={activeStatus} blur={blur} setBlur={setBlur} - requestData={testData} + requestData={testData as IncomingRequestProps} initiateAnimation={initiateAnimation} handleAccept={handleAccept} handleCancel={handleCancel} handleIgnore={handleIgnore} - incomingRequestType={IncomingRequestType.PEER_CONNECT_SIGN} /> ); @@ -301,12 +304,11 @@ describe("Sign JSON", () => { activeStatus={activeStatus} blur={blur} setBlur={setBlur} - requestData={requestData} + requestData={requestData as IncomingRequestProps} initiateAnimation={initiateAnimation} handleAccept={handleAccept} handleCancel={handleCancel} handleIgnore={handleIgnore} - incomingRequestType={IncomingRequestType.PEER_CONNECT_SIGN} /> ); @@ -330,12 +332,11 @@ describe("Sign JSON", () => { activeStatus={activeStatus} blur={blur} setBlur={setBlur} - requestData={testData} + requestData={testData as IncomingRequestProps} initiateAnimation={initiateAnimation} handleAccept={handleAccept} handleCancel={handleCancel} handleIgnore={handleIgnore} - incomingRequestType={IncomingRequestType.PEER_CONNECT_SIGN} /> ); diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.tsx index e4db9746d..e23fa0f05 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/RequestComponent.tsx @@ -14,9 +14,8 @@ const RequestComponent = ({ handleAccept, handleCancel, handleIgnore, - incomingRequestType, -}: RequestProps) => { - switch (incomingRequestType) { +}: RequestProps) => { + switch (requestData.type) { case IncomingRequestType.CREDENTIAL_OFFER_RECEIVED: return ( ); case IncomingRequestType.MULTI_SIG_REQUEST_INCOMING: diff --git a/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx b/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx index 326ca9f25..b176fe039 100644 --- a/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx +++ b/src/ui/pages/SidePage/components/IncomingRequest/components/SignRequest.tsx @@ -7,11 +7,12 @@ import { } from "../../../../../components/CardDetails"; import { PageFooter } from "../../../../../components/PageFooter"; import { ScrollablePageLayout } from "../../../../../components/layout/ScrollablePageLayout"; -import CardanoLogo from "../../../../../assets/images/CardanoLogo.jpg"; +import UserIcon from "../../../../../assets/images/KeriGeneric.jpg"; import { RequestProps } from "../IncomingRequest.types"; import "./SignRequest.scss"; import { Spinner } from "../../../../../components/Spinner"; import { PageHeader } from "../../../../../components/PageHeader"; +import { IncomingRequestType } from "../../../../../../store/reducers/stateCache/stateCache.types"; const SignRequest = ({ pageId, @@ -20,23 +21,25 @@ const SignRequest = ({ initiateAnimation, handleAccept, handleCancel, -}: RequestProps) => { - const signRequest = requestData.signTransaction; +}: RequestProps) => { const [isSigningObject, setIsSigningObject] = useState(false); - const logo = requestData.logo ? requestData.logo : CardanoLogo; - const signDetails = useMemo(() => { - if (!signRequest) return {}; + if (!requestData.signTransaction) { + return {}; + } let signContent; try { - signContent = JSON.parse(signRequest.payload.payload); + signContent = JSON.parse(requestData.signTransaction.payload.payload); setIsSigningObject(true); } catch (error) { - signContent = signRequest.payload.payload; + signContent = requestData.signTransaction.payload.payload; } return signContent; - }, [requestData.signTransaction]); + }, [requestData.type]); + + const signRequest = requestData.signTransaction; + const logo = requestData.peerConnection.iconB64 || UserIcon; const handleSign = () => { handleAccept(); From 354952be9817b34c60d1ed0c673b6d9855aa58b9 Mon Sep 17 00:00:00 2001 From: Patrick Nguyen Date: Wed, 12 Jun 2024 16:37:08 +0700 Subject: [PATCH 19/28] refactor: adjust experimental api to be KERI specific (#515) --- .../agent/services/identifierService.test.ts | 8 +++++-- src/core/agent/services/identifierService.ts | 5 +++-- .../identityWalletConnect.test.ts | 17 +++++++-------- .../walletConnect/identityWalletConnect.ts | 21 ++++++++++--------- .../walletConnect/peerConnection.test.ts | 11 ++++++++-- .../cardano/walletConnect/peerConnection.ts | 17 +++++++++------ .../walletConnect/peerConnection.types.ts | 5 ++--- 7 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/core/agent/services/identifierService.test.ts b/src/core/agent/services/identifierService.test.ts index cfd93f1ba..df14e424e 100644 --- a/src/core/agent/services/identifierService.test.ts +++ b/src/core/agent/services/identifierService.test.ts @@ -334,6 +334,10 @@ describe("Single sig service of agent", () => { identifierStorage.getIdentifierMetadata = jest .fn() .mockResolvedValue(archivedMetadataRecord); + PeerConnection.peerConnection.getConnectingIdentifier = jest + .fn() + .mockReturnValue({ id: archivedMetadataRecord.id, oobi: "oobi" }); + identifierStorage.updateIdentifierMetadata = jest.fn(); await identifierService.deleteIdentifier(archivedMetadataRecord.id); expect(identifierStorage.getIdentifierMetadata).toBeCalledWith( @@ -355,9 +359,9 @@ describe("Single sig service of agent", () => { PeerConnection.peerConnection.getConnectedDAppAddress = jest .fn() .mockReturnValue("dApp-address"); - PeerConnection.peerConnection.getConnectingAid = jest + PeerConnection.peerConnection.getConnectingIdentifier = jest .fn() - .mockReturnValue(archivedMetadataRecord.id); + .mockReturnValue({ id: archivedMetadataRecord.id, oobi: "oobi" }); await identifierService.deleteIdentifier(archivedMetadataRecord.id); expect(identifierStorage.getIdentifierMetadata).toBeCalledWith( archivedMetadataRecord.id diff --git a/src/core/agent/services/identifierService.ts b/src/core/agent/services/identifierService.ts index d3eb3339e..bb44c40b2 100644 --- a/src/core/agent/services/identifierService.ts +++ b/src/core/agent/services/identifierService.ts @@ -182,12 +182,13 @@ class IdentifierService extends AgentService { ); const connectedDApp = PeerConnection.peerConnection.getConnectedDAppAddress(); - const peerConnectingAid = PeerConnection.peerConnection.getConnectingAid(); + const peerConnectingIdentifier = + await PeerConnection.peerConnection.getConnectingIdentifier(); this.validArchivedIdentifier(metadata); await this.identifierStorage.updateIdentifierMetadata(identifier, { isDeleted: true, }); - if (connectedDApp !== "" && metadata.id === peerConnectingAid) { + if (connectedDApp !== "" && metadata.id === peerConnectingIdentifier.id) { PeerConnection.peerConnection.disconnectDApp(connectedDApp, true); } } diff --git a/src/core/cardano/walletConnect/identityWalletConnect.test.ts b/src/core/cardano/walletConnect/identityWalletConnect.test.ts index 24e591ef8..8139f6b81 100644 --- a/src/core/cardano/walletConnect/identityWalletConnect.test.ts +++ b/src/core/cardano/walletConnect/identityWalletConnect.test.ts @@ -51,7 +51,7 @@ describe("IdentityWalletConnect", () => { .fn() .mockResolvedValue(undefined); - await expect(identityWalletConnect.getIdentifierOobi()).rejects.toThrow( + await expect(identityWalletConnect.getKeriIdentifier()).rejects.toThrow( IdentityWalletConnect.IDENTIFIER_ID_NOT_LOCATED ); }); @@ -63,12 +63,9 @@ describe("IdentityWalletConnect", () => { .mockResolvedValue(mockIdentifier); Agent.agent.connections.getOobi = jest.fn().mockResolvedValue("test-oobi"); - const result = await identityWalletConnect.getIdentifierOobi(); - expect(result).toBe("test-oobi"); - }); - - test("should return connecting aid", () => { - expect(identityWalletConnect.getConnectingAid()).toBe(selectedAid); + const result = await identityWalletConnect.getKeriIdentifier(); + expect(result.oobi).toBe("test-oobi"); + expect(result.id).toBe(selectedAid); }); test("should sign payload if approved", async () => { @@ -85,7 +82,7 @@ describe("IdentityWalletConnect", () => { event.payload.approvalCallback(true); }); - const result = await identityWalletConnect.sign(identifier, payload); + const result = await identityWalletConnect.signKeri(identifier, payload); expect(result).toBe("signed-payload"); }); @@ -97,7 +94,7 @@ describe("IdentityWalletConnect", () => { eventServiceMock.emit = jest.fn(); jest.spyOn(global.Date, "now").mockImplementationOnce(() => 1); - const result = await identityWalletConnect.sign(identifier, payload); + const result = await identityWalletConnect.signKeri(identifier, payload); expect(result).toEqual({ error: TxSignError.TimeOut }); expect(eventServiceMock.emit).toHaveBeenCalledWith({ type: PeerConnectionEventTypes.PeerConnectSign, @@ -120,7 +117,7 @@ describe("IdentityWalletConnect", () => { event.payload.approvalCallback(false); }); - const result = await identityWalletConnect.sign(identifier, payload); + const result = await identityWalletConnect.signKeri(identifier, payload); expect(result).toEqual({ error: TxSignError.UserDeclined }); }); }); diff --git a/src/core/cardano/walletConnect/identityWalletConnect.ts b/src/core/cardano/walletConnect/identityWalletConnect.ts index 9263fa1cd..fabfe9ec3 100644 --- a/src/core/cardano/walletConnect/identityWalletConnect.ts +++ b/src/core/cardano/walletConnect/identityWalletConnect.ts @@ -22,12 +22,11 @@ class IdentityWalletConnect extends CardanoPeerConnect { private eventService: EventService; static readonly MAX_SIGN_TIME = 3600000; static readonly TIMEOUT_INTERVAL = 1000; - getIdentifierOobi: () => Promise; - sign: ( + getKeriIdentifier: () => Promise<{ id: string; oobi: string }>; + signKeri: ( identifier: string, payload: string ) => Promise; - getConnectingAid: () => string; signerCache: Map; @@ -49,17 +48,23 @@ class IdentityWalletConnect extends CardanoPeerConnect { this.signerCache = new Map(); this.eventService = eventService; - this.getIdentifierOobi = async (): Promise => { + this.getKeriIdentifier = async (): Promise<{ + id: string; + oobi: string; + }> => { const identifier = await Agent.agent.identifiers.getIdentifier( this.selectedAid ); if (!identifier) { throw new Error(IdentityWalletConnect.IDENTIFIER_ID_NOT_LOCATED); } - return Agent.agent.connections.getOobi(identifier.signifyName); + return { + id: this.selectedAid, + oobi: await Agent.agent.connections.getOobi(identifier.signifyName), + }; }; - this.sign = async ( + this.signKeri = async ( identifier: string, payload: string ): Promise => { @@ -99,10 +104,6 @@ class IdentityWalletConnect extends CardanoPeerConnect { return { error: TxSignError.UserDeclined }; } }; - - this.getConnectingAid = () => { - return this.selectedAid; - }; } protected getNetworkId(): Promise { diff --git a/src/core/cardano/walletConnect/peerConnection.test.ts b/src/core/cardano/walletConnect/peerConnection.test.ts index 7bc6c68b1..1ab27d9ca 100644 --- a/src/core/cardano/walletConnect/peerConnection.test.ts +++ b/src/core/cardano/walletConnect/peerConnection.test.ts @@ -15,8 +15,10 @@ jest.mock("../../agent/agent", () => ({ connections: { getConnectionShortDetailById: jest.fn(), getMultisigLinkedContacts: jest.fn(), + getOobi: jest.fn(), }, identifiers: { + getIdentifier: jest.fn(), updateIdentifier: jest.fn(), }, getKeriaOnlineStatus: jest.fn(), @@ -83,6 +85,11 @@ describe("PeerConnection", () => { test("should connect with a DApp if there is not existing connection", async () => { const dAppIdentifier = "testDApp"; + Agent.agent.identifiers.getIdentifier = jest.fn().mockResolvedValue({ + id: "id", + signifyName: "signifyName", + }); + Agent.agent.connections.getOobi = jest.fn().mockResolvedValue("test-oobi"); Agent.agent.peerConnectionMetadataStorage.getPeerConnectionMetadata = jest .fn() .mockRejectedValue( @@ -175,8 +182,8 @@ describe("PeerConnection", () => { expect(peerConnection.getConnectedDAppAddress()).toBe(""); }); - test("should return the connecting Aid", () => { + test("should return the connecting Aid", async () => { peerConnection.start("testAid"); - expect(peerConnection.getConnectingAid()).toBe("testAid"); + expect((await peerConnection.getConnectingIdentifier()).id).toBe("testAid"); }); }); diff --git a/src/core/cardano/walletConnect/peerConnection.ts b/src/core/cardano/walletConnect/peerConnection.ts index 1fd90b94c..4fff50a4b 100644 --- a/src/core/cardano/walletConnect/peerConnection.ts +++ b/src/core/cardano/walletConnect/peerConnection.ts @@ -163,9 +163,8 @@ class PeerConnection { this.identityWalletConnect.setEnableExperimentalApi( new ExperimentalContainer({ - getIdentifierOobi: this.identityWalletConnect.getIdentifierOobi, - sign: this.identityWalletConnect.sign, - getConnectingAid: this.identityWalletConnect.getConnectingAid, + getKeriIdentifier: this.identityWalletConnect.getKeriIdentifier, + signKeri: this.identityWalletConnect.signKeri, }) ); } @@ -188,10 +187,13 @@ class PeerConnection { } }); if (!existingPeerConnection) { + const connectingIdentifier = + await this.identityWalletConnect.getKeriIdentifier(); await Agent.agent.peerConnectionMetadataStorage.createPeerConnectionMetadataRecord( { id: dAppIdentifier, - selectedAid: this.identityWalletConnect.getConnectingAid(), + selectedAid: connectingIdentifier.id, + iconB64: ICON_BASE64, } ); } @@ -218,8 +220,11 @@ class PeerConnection { return this.connectedDAppAdress; } - getConnectingAid() { - return this.identityWalletConnect?.getConnectingAid(); + async getConnectingIdentifier() { + if (this.identityWalletConnect === undefined) { + throw new Error(PeerConnection.PEER_CONNECTION_START_PENDING); + } + return this.identityWalletConnect.getKeriIdentifier(); } } diff --git a/src/core/cardano/walletConnect/peerConnection.types.ts b/src/core/cardano/walletConnect/peerConnection.types.ts index c8025a72d..7a03bdfc9 100644 --- a/src/core/cardano/walletConnect/peerConnection.types.ts +++ b/src/core/cardano/walletConnect/peerConnection.types.ts @@ -4,12 +4,11 @@ interface BaseEventEmitter { } interface ExperimentalAPIFunctions { - getIdentifierOobi: () => Promise; - sign: ( + getKeriIdentifier: () => Promise<{ id: string; oobi: string }>; + signKeri: ( identifier: string, payload: string ) => Promise; - getConnectingAid: () => string; } enum PeerConnectionEventTypes { From 94a2dbf18caca94890bfc371c397863a295cfb2f Mon Sep 17 00:00:00 2001 From: Bao Hoang <142210850+bao-sotatek@users.noreply.github.com> Date: Thu, 13 Jun 2024 15:27:30 +0700 Subject: [PATCH 20/28] feat: dynamic attributes for all credential types (#516) * feat: pass dynamic attr when issue credential * chore: fix comment --- services/credential-issuance-server/src/agent.ts | 4 ++-- .../src/apis/credential.api.ts | 4 ++-- .../src/modules/signify/signifyApi.ts | 15 ++++++--------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/services/credential-issuance-server/src/agent.ts b/services/credential-issuance-server/src/agent.ts index 82f0b1ebe..d31e169d1 100644 --- a/services/credential-issuance-server/src/agent.ts +++ b/services/credential-issuance-server/src/agent.ts @@ -38,13 +38,13 @@ class Agent { return this.signifyApi.resolveOobi(url); } - async issueAcdcCredentialByAid(schemaSaid, aid, name?) { + async issueAcdcCredentialByAid(schemaSaid, aid, attribute) { return this.signifyApi.issueCredential( Agent.ISSUER_AID_NAME, this.keriRegistryRegk, schemaSaid, aid, - name + attribute ); } diff --git a/services/credential-issuance-server/src/apis/credential.api.ts b/services/credential-issuance-server/src/apis/credential.api.ts index 8a7c22675..f6ba2e3a0 100644 --- a/services/credential-issuance-server/src/apis/credential.api.ts +++ b/services/credential-issuance-server/src/apis/credential.api.ts @@ -5,7 +5,7 @@ import { httpResponse } from "../utils/response.util"; import { SCHEMA_ACDC } from "../utils/schemas/schemaAcdc"; async function issueAcdcCredential(req: Request, res: Response): Promise { - const { schemaSaid, aid, name } = req.body; + const { schemaSaid, aid, attribute } = req.body; if (!SCHEMA_ACDC[schemaSaid]) { const response: ResponseData = { statusCode: 409, @@ -14,7 +14,7 @@ async function issueAcdcCredential(req: Request, res: Response): Promise { }; return httpResponse(res, response); } - await Agent.agent.issueAcdcCredentialByAid(schemaSaid, aid, name); + await Agent.agent.issueAcdcCredentialByAid(schemaSaid, aid, attribute); const response: ResponseData = { statusCode: 200, success: true, diff --git a/services/credential-issuance-server/src/modules/signify/signifyApi.ts b/services/credential-issuance-server/src/modules/signify/signifyApi.ts index c3d7a5df8..80a684391 100644 --- a/services/credential-issuance-server/src/modules/signify/signifyApi.ts +++ b/services/credential-issuance-server/src/modules/signify/signifyApi.ts @@ -97,19 +97,16 @@ export class SignifyApi { registryId: string, schemaId: string, recipient: string, - name?: string + attribute: { [key: string]: string } ) { await this.resolveOobi(`${config.oobiEndpoint}/oobi/${schemaId}`); let vcdata = {}; - if (schemaId === "EBIFDhtSE0cM4nbTnaMqiV1vUIlcnbsqBMeVMmeGmXOu") { - vcdata = { - attendeeName: name, - }; - } else if (schemaId === "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao") { - vcdata = { - LEI: "5493001KJTIIGC8Y1R17", - }; + if ( + schemaId === "EBIFDhtSE0cM4nbTnaMqiV1vUIlcnbsqBMeVMmeGmXOu" || + schemaId === "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao" + ) { + vcdata = attribute; } else { throw new Error(SignifyApi.UNKNOW_SCHEMA_ID + schemaId); } From 603aa5a50aa910f0febb07e6c3d1aecf34e824e5 Mon Sep 17 00:00:00 2001 From: Bao Hoang <142210850+bao-sotatek@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:19:03 +0700 Subject: [PATCH 21/28] feat(core): recover existing KERIA agent (#517) * feat: add function recovery * feat: check boot URL not being called yet * chore: fix comment * chore: fix comment --- src/core/agent/agent.ts | 98 +++++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/src/core/agent/agent.ts b/src/core/agent/agent.ts index 1936e70ca..ce037d0d3 100644 --- a/src/core/agent/agent.ts +++ b/src/core/agent/agent.ts @@ -57,7 +57,9 @@ class Agent { static readonly KERIA_BOOT_FAILED = "Failed to boot signify client"; static readonly KERIA_BOOTED_ALREADY_BUT_CANNOT_CONNECT = "Signify client is already booted but cannot connect"; - + static readonly KERIA_NOT_BOOTED = + "Agent has not been booted for a given Signify passcode"; + static readonly INVALID_MNEMONIC = "Seed phrase is invalid"; private static instance: Agent; private agentServicesProps: AgentServicesProps = { eventService: undefined as any, @@ -238,32 +240,74 @@ class Agent { throw new Error(Agent.KERIA_BOOTED_ALREADY_BUT_CANNOT_CONNECT); } await this.saveAgentUrls(agentUrls); - Agent.isOnline = true; - this.agentServicesProps.signifyClient = this.signifyClient; - this.agentServicesProps.eventService.emit({ - type: KeriaStatusEventTypes.KeriaStatusChanged, - payload: { - isOnline: Agent.isOnline, - }, - }); + this.markAgentOnline(); } } + async recoverKeriaAgent( + seedPhrase: string[], + connectUrl: string + ): Promise { + let bran = ""; + try { + const mnemonic = seedPhrase.join(" "); + bran = Buffer.from(mnemonicToEntropy(mnemonic), "hex") + .toString("utf-8") + .replace(/\0/g, ""); - private async saveAgentUrls(agentUrls: AgentUrls): Promise { - await this.basicStorageService.save({ - id: MiscRecordId.KERIA_CONNECT_URL, - content: { - url: agentUrls.url, - }, + this.signifyClient = new SignifyClient(connectUrl, bran, Tier.low); + + await this.signifyClient.connect(); + } catch (error) { + if (error instanceof Error) { + if (error.message === "Invalid mnemonic") { + throw new Error(Agent.INVALID_MNEMONIC); + } + if (error.message.includes("agent does not exist for controller")) { + throw new Error(Agent.KERIA_NOT_BOOTED); + } + throw error; + } + } + + await SecureStorage.set(KeyStoreKeys.SIGNIFY_BRAN, bran); + await this.saveAgentUrls({ + url: connectUrl, + bootUrl: "", }); - await this.basicStorageService.save({ - id: MiscRecordId.KERIA_BOOT_URL, - content: { - url: agentUrls.bootUrl, + + this.markAgentOnline(); + } + + private markAgentOnline() { + Agent.isOnline = true; + this.agentServicesProps.signifyClient = this.signifyClient; + this.agentServicesProps.eventService.emit({ + type: KeriaStatusEventTypes.KeriaStatusChanged, + payload: { + isOnline: Agent.isOnline, }, }); } + private async saveAgentUrls(agentUrls: AgentUrls): Promise { + if (agentUrls.url) { + await this.basicStorageService.save({ + id: MiscRecordId.KERIA_CONNECT_URL, + content: { + url: agentUrls.url, + }, + }); + } + if (agentUrls.bootUrl) { + await this.basicStorageService.save({ + id: MiscRecordId.KERIA_BOOT_URL, + content: { + url: agentUrls.bootUrl, + }, + }); + } + } + async initDatabaseConnection(): Promise { await this.storageSession.open(walletId); this.basicStorageService = new BasicStorage( @@ -351,13 +395,19 @@ class Agent { } async isMnemonicValid(mnemonic: string): Promise { - const bran = (await SecureStorage.get(KeyStoreKeys.SIGNIFY_BRAN)) as string; - return ( - bran === + try { Buffer.from(mnemonicToEntropy(mnemonic), "hex") .toString("utf-8") - .replace(/\0/g, "") - ); + .replace(/\0/g, ""); + return true; + } catch (error) { + if (error instanceof Error) { + if (error.message === "Invalid mnemonic") { + return false; + } + } + throw error; + } } } From c5efa6c072fee78407081956183e4242a4578586 Mon Sep 17 00:00:00 2001 From: Fergal Date: Thu, 13 Jun 2024 13:21:55 +0100 Subject: [PATCH 22/28] build: commit missing changes to capacitor files (#519) --- android/app/capacitor.build.gradle | 2 +- android/capacitor.settings.gradle | 6 +++--- ios/App/App/Info.plist | 18 +++++++++--------- ios/App/Podfile | 2 +- ios/App/Podfile.lock | 14 +++++++------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 1aef8a6d3..240b76cfc 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -16,11 +16,11 @@ dependencies { implementation project(':capacitor-app') implementation project(':capacitor-clipboard') implementation project(':capacitor-keyboard') - implementation project(':capacitor-preferences') implementation project(':capacitor-screen-orientation') implementation project(':capacitor-share') implementation project(':capacitor-splash-screen') implementation project(':capacitor-status-bar') + implementation project(':capacitor-native-settings') } diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index ce250e31b..e595f0e98 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -23,9 +23,6 @@ project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacito include ':capacitor-keyboard' project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android') -include ':capacitor-preferences' -project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') - include ':capacitor-screen-orientation' project(':capacitor-screen-orientation').projectDir = new File('../node_modules/@capacitor/screen-orientation/android') @@ -37,3 +34,6 @@ project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capa include ':capacitor-status-bar' project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') + +include ':capacitor-native-settings' +project(':capacitor-native-settings').projectDir = new File('../node_modules/capacitor-native-settings/android') diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index b32a780a5..a62f4a5d7 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -2,8 +2,6 @@ - NSCameraUsageDescription - To be able to scan barcodes CFBundleDevelopmentRegion en CFBundleDisplayName @@ -26,6 +24,15 @@ LSSupportsOpeningDocumentsInPlace + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSCameraUsageDescription + To be able to scan barcodes + NSFaceIDUsageDescription + This app uses Face ID to secure your data and provide faster access. UIFileSharingEnabled UILaunchStoryboardName @@ -71,12 +78,5 @@ - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSFaceIDUsageDescription - This app uses Face ID to secure your data and provide faster access. diff --git a/ios/App/Podfile b/ios/App/Podfile index 4c5185583..3e3b59cd3 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -18,11 +18,11 @@ def capacitor_pods pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard' - pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' pod 'CapacitorScreenOrientation', :path => '../../node_modules/@capacitor/screen-orientation' pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' + pod 'CapacitorNativeSettings', :path => '../../node_modules/capacitor-native-settings' end target 'App' do diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 44aa821a3..96e5ce0c0 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -19,7 +19,7 @@ PODS: - CapacitorCordova (5.7.4) - CapacitorKeyboard (5.0.8): - Capacitor - - CapacitorPreferences (5.0.7): + - CapacitorNativeSettings (5.0.1): - Capacitor - CapacitorScreenOrientation (5.0.7): - Capacitor @@ -47,7 +47,7 @@ DEPENDENCIES: - "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)" - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorKeyboard (from `../../node_modules/@capacitor/keyboard`)" - - "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)" + - CapacitorNativeSettings (from `../../node_modules/capacitor-native-settings`) - "CapacitorScreenOrientation (from `../../node_modules/@capacitor/screen-orientation`)" - "CapacitorShare (from `../../node_modules/@capacitor/share`)" - "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)" @@ -78,8 +78,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/ios" CapacitorKeyboard: :path: "../../node_modules/@capacitor/keyboard" - CapacitorPreferences: - :path: "../../node_modules/@capacitor/preferences" + CapacitorNativeSettings: + :path: "../../node_modules/capacitor-native-settings" CapacitorScreenOrientation: :path: "../../node_modules/@capacitor/screen-orientation" CapacitorShare: @@ -99,7 +99,7 @@ SPEC CHECKSUMS: CapacitorCommunitySqlite: 6e2754dde799d618a8e75e409ccc67ec9c189460 CapacitorCordova: a6e87fccc0307dee7aec1560ec9398485f2b0ce7 CapacitorKeyboard: aec619a578235c6ce279075009a2689c2cf5c42c - CapacitorPreferences: 77ac427e98db83bace772455f8ba447430382c4c + CapacitorNativeSettings: 6e94ed3c0465206756f320df18efc52f053ce3c7 CapacitorScreenOrientation: cc638c369cb2b1dfc55f8265485a8b4e0b3cafd9 CapacitorShare: c6a1ebbf0114ff9e863b966cd6052678fa25d480 CapacitorSplashScreen: dd3de3f3644710fa2a697cfb91ec262eece4d242 @@ -108,6 +108,6 @@ SPEC CHECKSUMS: SQLCipher: f2e96b3822e3006b379181a0e4fd145f6de29b56 ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced -PODFILE CHECKSUM: 376fee91162e07308901c11bbed7e5bb1df1add6 +PODFILE CHECKSUM: 227349d5dcfc878b902c4a3bda2c6e701c6df134 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 From f556f621dfdcc84ad8fd71c39431a1265e6bf06e Mon Sep 17 00:00:00 2001 From: Patrick Nguyen Date: Fri, 14 Jun 2024 16:29:51 +0700 Subject: [PATCH 23/28] fix: hex identifier sn (#520) * refactor: hex sn * fix: change the compare syntax of the sn --- src/core/agent/services/identifier.types.ts | 2 +- src/ui/__fixtures__/identifierFix.ts | 2 +- .../pages/IdentifierDetails/components/IdentifierContent.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/agent/services/identifier.types.ts b/src/core/agent/services/identifier.types.ts index 526a4d075..5f709e379 100644 --- a/src/core/agent/services/identifier.types.ts +++ b/src/core/agent/services/identifier.types.ts @@ -24,7 +24,7 @@ interface IdentifierShortDetails { interface IdentifierDetails extends IdentifierShortDetails { signifyOpName?: string; - s: number; + s: string; dt: string; kt: number; k: string[]; diff --git a/src/ui/__fixtures__/identifierFix.ts b/src/ui/__fixtures__/identifierFix.ts index 0a5777e13..f5b18ad32 100644 --- a/src/ui/__fixtures__/identifierFix.ts +++ b/src/ui/__fixtures__/identifierFix.ts @@ -8,7 +8,7 @@ const identifierFix: IdentifierDetails[] = [ createdAtUTC: "2023-01-01T19:23:24Z", isPending: false, theme: 0, - s: 4, // Sequence number, only show if s > 0 + s: "4", // Sequence number, only show if s > 0 dt: "2023-06-12T14:07:53.224866+00:00", // Last key rotation timestamp, if s > 0 kt: 2, // Keys signing threshold (only show if kt > 1) k: [ diff --git a/src/ui/pages/IdentifierDetails/components/IdentifierContent.tsx b/src/ui/pages/IdentifierDetails/components/IdentifierContent.tsx index 1fb1ced68..0808f9e99 100644 --- a/src/ui/pages/IdentifierDetails/components/IdentifierContent.tsx +++ b/src/ui/pages/IdentifierDetails/components/IdentifierContent.tsx @@ -92,7 +92,7 @@ const IdentifierContent = ({ cardData }: IdentifierContentProps) => { testId="creation-timestamp" /> - {cardData.s > 0 && cardData.dt && ( + {cardData.s !== "0" && cardData.dt && ( @@ -108,7 +108,7 @@ const IdentifierContent = ({ cardData }: IdentifierContentProps) => { /> )} - {cardData.s > 0 && ( + {cardData.s !== "0" && ( From 4ddd1e223a66f3f866caf2053e86f158675d9fb1 Mon Sep 17 00:00:00 2001 From: iFergal Date: Fri, 14 Jun 2024 13:24:29 +0100 Subject: [PATCH 24/28] feat: re-introduce preferences temp (until we handle APP_ALREADY_INIT differently) --- android/app/capacitor.build.gradle | 1 + android/capacitor.settings.gradle | 3 ++ ios/App/Podfile | 1 + ios/App/Podfile.lock | 8 +++- package-lock.json | 9 +++++ package.json | 1 + src/core/agent/agent.ts | 4 +- .../storage/preferences/preferencesStorage.ts | 38 +++++++++++++++++++ .../preferences/preferencesStorage.types.ts | 5 +++ src/ui/components/AppWrapper/AppWrapper.tsx | 13 +++++-- src/ui/pages/SetPasscode/SetPasscode.test.tsx | 31 ++++++++------- src/ui/pages/SetPasscode/SetPasscode.tsx | 19 ++++------ 12 files changed, 101 insertions(+), 32 deletions(-) create mode 100644 src/core/storage/preferences/preferencesStorage.ts create mode 100644 src/core/storage/preferences/preferencesStorage.types.ts diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 240b76cfc..7e1ab259c 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -16,6 +16,7 @@ dependencies { implementation project(':capacitor-app') implementation project(':capacitor-clipboard') implementation project(':capacitor-keyboard') + implementation project(':capacitor-preferences') implementation project(':capacitor-screen-orientation') implementation project(':capacitor-share') implementation project(':capacitor-splash-screen') diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index e595f0e98..961770e71 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -23,6 +23,9 @@ project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacito include ':capacitor-keyboard' project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android') +include ':capacitor-preferences' +project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') + include ':capacitor-screen-orientation' project(':capacitor-screen-orientation').projectDir = new File('../node_modules/@capacitor/screen-orientation/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index 3e3b59cd3..81c0cdc66 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -18,6 +18,7 @@ def capacitor_pods pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard' + pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' pod 'CapacitorScreenOrientation', :path => '../../node_modules/@capacitor/screen-orientation' pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 96e5ce0c0..2ef1d4d8c 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -21,6 +21,8 @@ PODS: - Capacitor - CapacitorNativeSettings (5.0.1): - Capacitor + - CapacitorPreferences (5.0.8): + - Capacitor - CapacitorScreenOrientation (5.0.7): - Capacitor - CapacitorShare (5.0.7): @@ -48,6 +50,7 @@ DEPENDENCIES: - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorKeyboard (from `../../node_modules/@capacitor/keyboard`)" - CapacitorNativeSettings (from `../../node_modules/capacitor-native-settings`) + - "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)" - "CapacitorScreenOrientation (from `../../node_modules/@capacitor/screen-orientation`)" - "CapacitorShare (from `../../node_modules/@capacitor/share`)" - "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)" @@ -80,6 +83,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/keyboard" CapacitorNativeSettings: :path: "../../node_modules/capacitor-native-settings" + CapacitorPreferences: + :path: "../../node_modules/@capacitor/preferences" CapacitorScreenOrientation: :path: "../../node_modules/@capacitor/screen-orientation" CapacitorShare: @@ -100,6 +105,7 @@ SPEC CHECKSUMS: CapacitorCordova: a6e87fccc0307dee7aec1560ec9398485f2b0ce7 CapacitorKeyboard: aec619a578235c6ce279075009a2689c2cf5c42c CapacitorNativeSettings: 6e94ed3c0465206756f320df18efc52f053ce3c7 + CapacitorPreferences: 9e59596c5fd9915ed45279d1fda68978c5706401 CapacitorScreenOrientation: cc638c369cb2b1dfc55f8265485a8b4e0b3cafd9 CapacitorShare: c6a1ebbf0114ff9e863b966cd6052678fa25d480 CapacitorSplashScreen: dd3de3f3644710fa2a697cfb91ec262eece4d242 @@ -108,6 +114,6 @@ SPEC CHECKSUMS: SQLCipher: f2e96b3822e3006b379181a0e4fd145f6de29b56 ZIPFoundation: d170fa8e270b2a32bef9dcdcabff5b8f1a5deced -PODFILE CHECKSUM: 227349d5dcfc878b902c4a3bda2c6e701c6df134 +PODFILE CHECKSUM: f17d8c92af7514371ab66701e64dde4c13796c54 COCOAPODS: 1.15.2 diff --git a/package-lock.json b/package-lock.json index 35a77376a..3c6f5fe2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@capacitor/core": "^5.0.0", "@capacitor/ios": "^5.0.0", "@capacitor/keyboard": "^5.0.0", + "@capacitor/preferences": "^5.0.0", "@capacitor/screen-orientation": "^5.0.7", "@capacitor/share": "^5.0.0", "@capacitor/splash-screen": "^5.0.0", @@ -2541,6 +2542,14 @@ "@capacitor/core": "^5.0.0" } }, + "node_modules/@capacitor/preferences": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-5.0.8.tgz", + "integrity": "sha512-zzz8JC2NuZ+xdBP2Cfhu4uyRUMAFoxMl7l8w5ahQPzckyt7Fk/pWATXj6IcTm7DzbsKc8ryXSsYTkv9ZL3Pfmw==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/@capacitor/screen-orientation": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/@capacitor/screen-orientation/-/screen-orientation-5.0.7.tgz", diff --git a/package.json b/package.json index 099378222..ca07688dc 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@capacitor/core": "^5.0.0", "@capacitor/ios": "^5.0.0", "@capacitor/keyboard": "^5.0.0", + "@capacitor/preferences": "^5.0.0", "@capacitor/screen-orientation": "^5.0.7", "@capacitor/share": "^5.0.0", "@capacitor/splash-screen": "^5.0.0", diff --git a/src/core/agent/agent.ts b/src/core/agent/agent.ts index ce037d0d3..4bab0f275 100644 --- a/src/core/agent/agent.ts +++ b/src/core/agent/agent.ts @@ -5,7 +5,6 @@ import { ready as signifyReady, Tier, } from "signify-ts"; -import { DataType } from "@aparajita/capacitor-secure-storage"; import { entropyToMnemonic, mnemonicToEntropy } from "bip39"; import { ConnectionService, @@ -46,7 +45,6 @@ import { IonicSession } from "../storage/ionicStorage/ionicSession"; import { IonicStorage } from "../storage/ionicStorage"; import { SqliteStorage } from "../storage/sqliteStorage"; import { BaseRecord } from "../storage/storage.types"; -import { ConfigurationService } from "../configuration"; import { OperationPendingStorage } from "./records/operationPendingStorage"; import { OperationPendingRecord } from "./records/operationPendingRecord"; @@ -60,6 +58,7 @@ class Agent { static readonly KERIA_NOT_BOOTED = "Agent has not been booted for a given Signify passcode"; static readonly INVALID_MNEMONIC = "Seed phrase is invalid"; + private static instance: Agent; private agentServicesProps: AgentServicesProps = { eventService: undefined as any, @@ -243,6 +242,7 @@ class Agent { this.markAgentOnline(); } } + async recoverKeriaAgent( seedPhrase: string[], connectUrl: string diff --git a/src/core/storage/preferences/preferencesStorage.ts b/src/core/storage/preferences/preferencesStorage.ts new file mode 100644 index 000000000..a437e93b3 --- /dev/null +++ b/src/core/storage/preferences/preferencesStorage.ts @@ -0,0 +1,38 @@ +import { Preferences } from "@capacitor/preferences"; +import { + RemoveOptions, + SetOptions, +} from "@capacitor/preferences/dist/esm/definitions"; +import { PreferencesStorageItem } from "./preferencesStorage.types"; + +enum PreferencesKeys { + APP_ALREADY_INIT = "app-already-init", +} + +class PreferencesStorage { + static readonly KEY_NOT_FOUND = + "Preferences Storage does not contain an item with specified key"; + static readonly INVALID_OBJECT = "Object format cannot be parsed"; + + static async get(key: string): Promise { + const item = await Preferences.get({ key }); + if (!item || !item?.value) { + throw new Error(`${PreferencesStorage.KEY_NOT_FOUND} ${key}`); + } + return JSON.parse(item.value); + } + + static async set(key: string, obj: PreferencesStorageItem): Promise { + const objStr: string = JSON.stringify(obj); + await Preferences.set({ + key, + value: objStr, + } as SetOptions); + } + + static async remove(key: string): Promise { + await Preferences.remove({ key } as RemoveOptions); + } +} + +export { PreferencesStorage, PreferencesKeys }; diff --git a/src/core/storage/preferences/preferencesStorage.types.ts b/src/core/storage/preferences/preferencesStorage.types.ts new file mode 100644 index 000000000..879779840 --- /dev/null +++ b/src/core/storage/preferences/preferencesStorage.types.ts @@ -0,0 +1,5 @@ +interface PreferencesStorageItem { + [key: string]: string | number | boolean | Array; +} + +export type { PreferencesStorageItem }; diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index ce49e93ef..7a76555f8 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -62,6 +62,10 @@ import { setCredsArchivedCache } from "../../../store/reducers/credsArchivedCach import { OperationPendingRecordType } from "../../../core/agent/records/operationPendingRecord.type"; import { i18n } from "../../../i18n"; import { Alert } from "../Alert"; +import { + PreferencesKeys, + PreferencesStorage, +} from "../../../core/storage/preferences/preferencesStorage"; const connectionStateChangedHandler = async ( event: ConnectionStateChangedEvent, @@ -379,10 +383,11 @@ const AppWrapper = (props: { children: ReactNode }) => { await Agent.agent.initDatabaseConnection(); // @TODO - foconnor: This is a temp hack for development to be removed pre-release. // These items are removed from the secure storage on re-install to re-test the on-boarding for iOS devices. - const appAlreadyInit = await Agent.agent.basicStorage.findById( - MiscRecordId.APP_ALREADY_INIT - ); - if (!appAlreadyInit) { + try { + // @TODO - foconnor: This should use our normal DB - keeping Preferences temporarily to not break existing mobile builds. + // Will remove preferences again once we have better handling on APP_ALREADY_INIT with user input. + await PreferencesStorage.get(PreferencesKeys.APP_ALREADY_INIT); + } catch (e) { await SecureStorage.delete(KeyStoreKeys.APP_PASSCODE); await SecureStorage.delete(KeyStoreKeys.APP_OP_PASSWORD); await SecureStorage.delete(KeyStoreKeys.SIGNIFY_BRAN); diff --git a/src/ui/pages/SetPasscode/SetPasscode.test.tsx b/src/ui/pages/SetPasscode/SetPasscode.test.tsx index 55c0ff6aa..a255e3e58 100644 --- a/src/ui/pages/SetPasscode/SetPasscode.test.tsx +++ b/src/ui/pages/SetPasscode/SetPasscode.test.tsx @@ -24,8 +24,15 @@ import { store } from "../../../store"; import { RoutePath } from "../../../routes"; import { MiscRecordId } from "../../../core/agent/agent.types"; import { Agent } from "../../../core/agent/agent"; +import { + PreferencesKeys, + PreferencesStorage, +} from "../../../core/storage/preferences/preferencesStorage"; const setKeyStoreSpy = jest.spyOn(SecureStorage, "set").mockResolvedValue(); +const setPreferenceStorageSpy = jest + .spyOn(PreferencesStorage, "set") + .mockResolvedValue(); jest.mock("../../../core/agent/agent", () => ({ Agent: { @@ -343,13 +350,11 @@ describe("SetPasscode Page", () => { }); expect(setKeyStoreSpy).toBeCalledWith(KeyStoreKeys.APP_PASSCODE, "111111"); - expect(Agent.agent.basicStorage.createOrUpdateBasicRecord).toBeCalledWith( - expect.objectContaining({ - id: MiscRecordId.APP_ALREADY_INIT, - content: { - initialized: true, - }, - }) + expect(setPreferenceStorageSpy).toBeCalledWith( + PreferencesKeys.APP_ALREADY_INIT, + { + initialized: true, + } ); }); @@ -461,13 +466,11 @@ describe("SetPasscode Page", () => { await waitFor(() => expect(setKeyStoreSpy).toBeCalledWith(KeyStoreKeys.APP_PASSCODE, "111111") ); - expect(Agent.agent.basicStorage.createOrUpdateBasicRecord).toBeCalledWith( - expect.objectContaining({ - id: MiscRecordId.APP_ALREADY_INIT, - content: { - initialized: true, - }, - }) + expect(setPreferenceStorageSpy).toBeCalledWith( + PreferencesKeys.APP_ALREADY_INIT, + { + initialized: true, + } ); }); diff --git a/src/ui/pages/SetPasscode/SetPasscode.tsx b/src/ui/pages/SetPasscode/SetPasscode.tsx index edf3046e2..530f05b0f 100644 --- a/src/ui/pages/SetPasscode/SetPasscode.tsx +++ b/src/ui/pages/SetPasscode/SetPasscode.tsx @@ -3,11 +3,7 @@ import { i18n } from "../../../i18n"; import { ErrorMessage } from "../../components/ErrorMessage"; import { SecureStorage, KeyStoreKeys } from "../../../core/storage"; import { PasscodeModule } from "../../components/PasscodeModule"; -import { - getStateCache, - setInitialized, - setToastMsg, -} from "../../../store/reducers/stateCache"; +import { getStateCache, setToastMsg } from "../../../store/reducers/stateCache"; import { useAppDispatch, useAppSelector } from "../../../store/hooks"; import { getNextRoute } from "../../../routes/nextRoute"; import { updateReduxState } from "../../../store/utils"; @@ -28,6 +24,10 @@ import { MiscRecordId } from "../../../core/agent/agent.types"; import { BasicRecord } from "../../../core/agent/records"; import { setEnableBiometryCache } from "../../../store/reducers/biometryCache"; import { ToastMsgType } from "../../globals/types"; +import { + PreferencesKeys, + PreferencesStorage, +} from "../../../core/storage/preferences/preferencesStorage"; const SetPasscode = () => { const pageId = "set-passcode"; @@ -114,12 +114,9 @@ const SetPasscode = () => { ionRouter.push(nextPath.pathname, "forward", "push"); handleClearState(); - await Agent.agent.basicStorage.createOrUpdateBasicRecord( - new BasicRecord({ - id: MiscRecordId.APP_ALREADY_INIT, - content: { initialized: true }, - }) - ); + await PreferencesStorage.set(PreferencesKeys.APP_ALREADY_INIT, { + initialized: true, + }); }; const handleSetupAndroidBiometry = async () => { From c421552d763ba6cb54c27bc285a77bacc27201de Mon Sep 17 00:00:00 2001 From: iFergal Date: Fri, 14 Jun 2024 13:25:01 +0100 Subject: [PATCH 25/28] fix(core): dont require meerkat to be launched to delete identifier --- src/core/agent/services/identifierService.ts | 12 +++++++----- src/core/cardano/walletConnect/peerConnection.ts | 12 ++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/core/agent/services/identifierService.ts b/src/core/agent/services/identifierService.ts index bb44c40b2..f16886e4a 100644 --- a/src/core/agent/services/identifierService.ts +++ b/src/core/agent/services/identifierService.ts @@ -180,15 +180,17 @@ class IdentifierService extends AgentService { const metadata = await this.identifierStorage.getIdentifierMetadata( identifier ); - const connectedDApp = - PeerConnection.peerConnection.getConnectedDAppAddress(); - const peerConnectingIdentifier = - await PeerConnection.peerConnection.getConnectingIdentifier(); this.validArchivedIdentifier(metadata); await this.identifierStorage.updateIdentifierMetadata(identifier, { isDeleted: true, }); - if (connectedDApp !== "" && metadata.id === peerConnectingIdentifier.id) { + const connectedDApp = + PeerConnection.peerConnection.getConnectedDAppAddress(); + if ( + connectedDApp !== "" && + metadata.id === + (await PeerConnection.peerConnection.getConnectingIdentifier()).id + ) { PeerConnection.peerConnection.disconnectDApp(connectedDApp, true); } } diff --git a/src/core/cardano/walletConnect/peerConnection.ts b/src/core/cardano/walletConnect/peerConnection.ts index 4fff50a4b..29db1eca6 100644 --- a/src/core/cardano/walletConnect/peerConnection.ts +++ b/src/core/cardano/walletConnect/peerConnection.ts @@ -38,7 +38,7 @@ class PeerConnection { ]; private identityWalletConnect: IdentityWalletConnect | undefined; - private connectedDAppAdress = ""; + private connectedDAppAddress = ""; private eventService = new EventService(); private static instance: PeerConnection; @@ -103,9 +103,9 @@ class PeerConnection { } if ( this.identityWalletConnect && - this.connectedDAppAdress.trim().length !== 0 + this.connectedDAppAddress.trim().length !== 0 ) { - this.disconnectDApp(this.connectedDAppAdress); + this.disconnectDApp(this.connectedDAppAddress); } this.identityWalletConnect = new IdentityWalletConnect( this.walletInfo, @@ -118,7 +118,7 @@ class PeerConnection { async (connectMessage: IConnectMessage) => { if (!connectMessage.error) { const { name, url, address, icon } = connectMessage.dApp; - this.connectedDAppAdress = address; + this.connectedDAppAddress = address; let iconB64; // Check if the icon is base64 if ( @@ -151,7 +151,7 @@ class PeerConnection { this.identityWalletConnect.setOnDisconnect( (disConnectMessage: IConnectMessage) => { - this.connectedDAppAdress = ""; + this.connectedDAppAddress = ""; this.eventService.emit({ type: PeerConnectionEventTypes.PeerDisconnected, payload: { @@ -217,7 +217,7 @@ class PeerConnection { } getConnectedDAppAddress() { - return this.connectedDAppAdress; + return this.connectedDAppAddress; } async getConnectingIdentifier() { From 48feccb0bd043dc71505d847086f32a97a1d457c Mon Sep 17 00:00:00 2001 From: Sotatek-DukeVu <162310763+Sotatek-DukeVu@users.noreply.github.com> Date: Mon, 17 Jun 2024 15:33:22 +0700 Subject: [PATCH 26/28] feat(ui): I already have a wallet (#521) * feat(ui): update redux store and router when onboarding * feat(ui): recovery wallet * feat(ui): update logic verify seed phrase * feat(ui): update ssi agent description * fix(ui): remove fail attemp after verify seed phrase success * fix(ui): fix failed unit test * feat(ui): catch ssi boot error * feat(ui): add space for page title * feat(ui): update behaviour of back button --------- Co-authored-by: Vu Van Duc --- .../storage/secureStorage/secureStorage.ts | 3 + src/locales/en/en.json | 37 ++ src/routes/backRoute/backRoute.test.ts | 4 +- src/routes/backRoute/backRoute.ts | 77 ++- src/routes/index.tsx | 7 + src/routes/nextRoute/nextRoute.test.ts | 2 + src/routes/nextRoute/nextRoute.ts | 52 +- src/routes/paths.ts | 1 + .../reducers/stateCache/stateCache.test.ts | 1 + src/store/reducers/stateCache/stateCache.ts | 3 +- .../reducers/stateCache/stateCache.types.ts | 1 + src/ui/App.tsx | 4 +- src/ui/components/AppWrapper/AppWrapper.tsx | 8 + .../SeedPhraseModule/SeedPhraseModule.scss | 21 + .../SeedPhraseModule/SeedPhraseModule.tsx | 192 ++++--- .../SeedPhraseModule.types.ts | 11 +- .../pages/CreatePassword/CreatePassword.tsx | 2 + .../CreateSSIAgent/CreateSSIAgent.test.tsx | 150 +++++- .../pages/CreateSSIAgent/CreateSSIAgent.tsx | 171 +++++-- src/ui/pages/Onboarding/Onboarding.tsx | 13 +- .../VerifyRecoverySeedPhrase.scss | 93 ++++ .../VerifyRecoverySeedPhrase.test.tsx | 368 +++++++++++++ .../VerifyRecoverySeedPhrase.tsx | 484 ++++++++++++++++++ .../VerifyRecoverySeedPhrase.types.ts | 6 + .../pages/VerifyRecoverySeedPhrase/index.ts | 1 + .../VerifySeedPhrase.test.tsx | 2 +- 26 files changed, 1578 insertions(+), 136 deletions(-) create mode 100644 src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.scss create mode 100644 src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx create mode 100644 src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.tsx create mode 100644 src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.types.ts create mode 100644 src/ui/pages/VerifyRecoverySeedPhrase/index.ts diff --git a/src/core/storage/secureStorage/secureStorage.ts b/src/core/storage/secureStorage/secureStorage.ts index f18d90c89..09ac8bfd7 100644 --- a/src/core/storage/secureStorage/secureStorage.ts +++ b/src/core/storage/secureStorage/secureStorage.ts @@ -6,8 +6,11 @@ import { enum KeyStoreKeys { APP_PASSCODE = "app-login-passcode", APP_OP_PASSWORD = "app-operations-password", + PASSWORD_SKIPPED = "app-password-skip", SIGNIFY_BRAN = "signify-bran", MEERKAT_SEED = "app-meerkat-seed", + RECOVERY_WALLET = "recovery-wallet", + RECOVERY_WALLET_LAST_FAIL_ATTEMPT_TIME = "recovery-wallet-last-fail-attempt-time", } class SecureStorage { diff --git a/src/locales/en/en.json b/src/locales/en/en.json index 9296d6bcc..54f662f62 100644 --- a/src/locales/en/en.json +++ b/src/locales/en/en.json @@ -588,6 +588,42 @@ } } }, + "verifyrecoveryseedphrase": { + "title": "Recover wallet", + "button": { + "continue": "Verify", + "lock": "Try again in 1 minute", + "clear": "Clear all" + }, + "paragraph": { + "top": "Please verify your seed phrase to recover your wallet. To start typing click on the first option." + }, + "suggestions": { + "title": "Suggestions", + "error": "All words must be compatible with the suggestions" + }, + "alert": { + "fail": { + "text": "Sorry, the seed phrase you have entered is incorrect!", + "button": { + "confirm": "Try again" + } + }, + "clear": { + "text": "Are you sure you want to clear all the words you have entered so far?", + "button": { + "confirm": "Clear all", + "cancel": "Cancel" + } + }, + "toomanyattempts": { + "text": "Too many failed attempts. Please try again later.", + "button": { + "confirm": "Ok" + } + } + } + }, "tabsmenu": { "label": { "identifiers": "Identity", @@ -643,6 +679,7 @@ "ssiagent": { "title": "Enter your SSI agent details", "description": "To continue, please enter the SSI agent boot and connect URLs (in your email or from your command line).", + "verifydescription": "To continue, please enter the SSI agent connect URLs (in your email or from your command line).", "button": { "info": "Get more information", "validate": "Validate" diff --git a/src/routes/backRoute/backRoute.test.ts b/src/routes/backRoute/backRoute.test.ts index 5e9ee7da9..52d4cf8dc 100644 --- a/src/routes/backRoute/backRoute.test.ts +++ b/src/routes/backRoute/backRoute.test.ts @@ -38,6 +38,7 @@ describe("getBackRoute", () => { userName: "", time: 0, ssiAgentIsSet: false, + recoveryWalletProgress: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { @@ -98,7 +99,7 @@ describe("getBackRoute", () => { const result = getBackRoute(currentPath, data); expect(result.backPath).toEqual({ pathname: "/route2" }); - expect(result.updateRedux).toHaveLength(3); + expect(result.updateRedux).toHaveLength(4); }); test("should return the correct back path when currentPath is /verifyseedphrase", () => { @@ -165,6 +166,7 @@ describe("getPreviousRoute", () => { userName: "", time: 0, ssiAgentIsSet: false, + recoveryWalletProgress: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { diff --git a/src/routes/backRoute/backRoute.ts b/src/routes/backRoute/backRoute.ts index f1f870cfe..9e5303860 100644 --- a/src/routes/backRoute/backRoute.ts +++ b/src/routes/backRoute/backRoute.ts @@ -1,4 +1,5 @@ import { AnyAction, ThunkAction } from "@reduxjs/toolkit"; +import { SecureStorage } from "@aparajita/capacitor-secure-storage"; import { RootState } from "../../store"; import { removeCurrentRoute, @@ -8,6 +9,56 @@ import { import { clearSeedPhraseCache } from "../../store/reducers/seedPhraseCache"; import { DataProps, PayloadProps } from "../nextRoute/nextRoute.types"; import { RoutePath, TabsRoutePath } from "../paths"; +import { KeyStoreKeys } from "../../core/storage"; + +const getDefaultPreviousPath = (path: string, data: DataProps) => { + const isRecoveryMode = + data.store.stateCache.authentication.recoveryWalletProgress; + + if (RoutePath.SSI_AGENT === path) { + return isRecoveryMode + ? RoutePath.VERIFY_RECOVERY_SEED_PHRASE + : RoutePath.GENERATE_SEED_PHRASE; + } + + if (RoutePath.VERIFY_SEED_PHRASE === path) { + return RoutePath.GENERATE_SEED_PHRASE; + } + + if ( + [ + RoutePath.VERIFY_RECOVERY_SEED_PHRASE, + RoutePath.GENERATE_SEED_PHRASE, + ].includes(path as RoutePath) + ) { + return RoutePath.CREATE_PASSWORD; + } + + return RoutePath.ONBOARDING; +}; + +const clearSecureStore = (path: string) => { + if ( + [ + RoutePath.VERIFY_RECOVERY_SEED_PHRASE, + RoutePath.GENERATE_SEED_PHRASE, + ].includes(path as RoutePath) + ) { + SecureStorage.remove(KeyStoreKeys.PASSWORD_SKIPPED); + SecureStorage.remove(KeyStoreKeys.APP_OP_PASSWORD); + return; + } + + if (path === RoutePath.CREATE_PASSWORD) { + SecureStorage.remove(KeyStoreKeys.RECOVERY_WALLET); + return; + } + + if (path === RoutePath.SSI_AGENT) { + SecureStorage.remove(KeyStoreKeys.SIGNIFY_BRAN); + return; + } +}; const getBackRoute = ( currentPath: string, @@ -17,6 +68,7 @@ const getBackRoute = ( updateRedux: (() => ThunkAction)[]; } => { const { updateRedux } = backRoute[currentPath]; + clearSecureStore(currentPath); return { backPath: backPath(data), @@ -31,11 +83,12 @@ const updateStoreSetCurrentRoute = (data: DataProps) => { if (prevPath) { path = prevPath.path; } else { - path = data.store.stateCache.routes[0].path; + path = getDefaultPreviousPath(data.store.stateCache.routes[0].path, data); } return setCurrentRoute({ path }); }; + const getPreviousRoute = (data: DataProps): { pathname: string } => { const routes = data.store.stateCache.routes; @@ -47,7 +100,7 @@ const getPreviousRoute = (data: DataProps): { pathname: string } => { } else if (prevPath) { path = prevPath.path; } else { - path = routes[0].path; + path = getDefaultPreviousPath(routes[0].path, data); } return { pathname: path }; @@ -62,6 +115,14 @@ const calcPreviousRoute = ( const backPath = (data: DataProps) => getPreviousRoute(data); +const clearPasswordState = (data: DataProps) => { + return setAuthentication({ + ...data.store.stateCache.authentication, + passwordIsSkipped: false, + passwordIsSet: false, + }); +}; + const backRoute: Record = { [RoutePath.ROOT]: { updateRedux: [], @@ -74,19 +135,27 @@ const backRoute: Record = { removeCurrentRoute, updateStoreSetCurrentRoute, clearSeedPhraseCache, + clearPasswordState, ], }, [RoutePath.VERIFY_SEED_PHRASE]: { updateRedux: [removeCurrentRoute, updateStoreSetCurrentRoute], }, + [RoutePath.VERIFY_RECOVERY_SEED_PHRASE]: { + updateRedux: [ + removeCurrentRoute, + updateStoreSetCurrentRoute, + clearPasswordState, + ], + }, [RoutePath.SSI_AGENT]: { - updateRedux: [], + updateRedux: [removeCurrentRoute, updateStoreSetCurrentRoute], }, [RoutePath.SET_PASSCODE]: { updateRedux: [removeCurrentRoute, updateStoreSetCurrentRoute], }, [RoutePath.CREATE_PASSWORD]: { - updateRedux: [], + updateRedux: [removeCurrentRoute, updateStoreSetCurrentRoute], }, [RoutePath.CONNECTION_DETAILS]: { updateRedux: [removeCurrentRoute], diff --git a/src/routes/index.tsx b/src/routes/index.tsx index f32771248..a5e74f71d 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -20,6 +20,7 @@ import { IdentifierDetails } from "../ui/pages/IdentifierDetails"; import { CredentialDetails } from "../ui/pages/CredentialDetails"; import { ConnectionDetails } from "../ui/pages/ConnectionDetails"; import { CreateSSIAgent } from "../ui/pages/CreateSSIAgent"; +import { VerifyRecoverySeedPhrase } from "../ui/pages/VerifyRecoverySeedPhrase"; const Routes = () => { const stateCache = useAppSelector(getStateCache); @@ -73,6 +74,12 @@ const Routes = () => { exact /> + + { passwordIsSet: false, passwordIsSkipped: true, ssiAgentIsSet: false, + recoveryWalletProgress: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { @@ -166,6 +167,7 @@ describe("getNextRoute", () => { passwordIsSet: false, passwordIsSkipped: true, ssiAgentIsSet: false, + recoveryWalletProgress: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { diff --git a/src/routes/nextRoute/nextRoute.ts b/src/routes/nextRoute/nextRoute.ts index 7bc153d0a..0b85720b4 100644 --- a/src/routes/nextRoute/nextRoute.ts +++ b/src/routes/nextRoute/nextRoute.ts @@ -16,15 +16,24 @@ import { ToastMsgType } from "../../ui/globals/types"; const getNextRootRoute = (store: StoreState) => { const authentication = store.stateCache.authentication; - let path; - if ( - authentication.passcodeIsSet && - authentication.seedPhraseIsSet && - authentication.ssiAgentIsSet - ) { + let path = RoutePath.ONBOARDING; + + if (authentication.passcodeIsSet) { + path = RoutePath.CREATE_PASSWORD; + } + + if (authentication.passwordIsSet || authentication.passwordIsSkipped) { + path = authentication.recoveryWalletProgress + ? RoutePath.VERIFY_RECOVERY_SEED_PHRASE + : RoutePath.GENERATE_SEED_PHRASE; + } + + if (authentication.seedPhraseIsSet) { + path = RoutePath.SSI_AGENT; + } + + if (authentication.ssiAgentIsSet) { path = RoutePath.TABS_MENU; - } else { - path = RoutePath.ONBOARDING; } return { pathname: path }; @@ -38,7 +47,9 @@ const getNextOnboardingRoute = (data: DataProps) => { } if (data.store.stateCache.authentication.passwordIsSet) { - path = RoutePath.GENERATE_SEED_PHRASE; + path = data.state?.recoveryWalletProgress + ? RoutePath.VERIFY_RECOVERY_SEED_PHRASE + : RoutePath.GENERATE_SEED_PHRASE; } if (data.store.stateCache.authentication.seedPhraseIsSet) { @@ -104,6 +115,15 @@ const updateStoreAfterSetupSSI = (data: DataProps) => { return setAuthentication({ ...data.store.stateCache.authentication, ssiAgentIsSet: true, + recoveryWalletProgress: false, + seedPhraseIsSet: true, + }); +}; + +const updateStoreRecoveryWallet = (data: DataProps) => { + return setAuthentication({ + ...data.store.stateCache.authentication, + recoveryWalletProgress: data.state?.recoveryWalletProgress, }); }; @@ -131,7 +151,11 @@ const updateStoreCurrentRoute = (data: DataProps) => { return setCurrentRoute({ path: data.state?.nextRoute }); }; -const getNextCreatePasswordRoute = () => { +const getNextCreatePasswordRoute = (data: DataProps) => { + if (data.store.stateCache.authentication.recoveryWalletProgress) { + return { pathname: RoutePath.VERIFY_RECOVERY_SEED_PHRASE }; + } + return { pathname: RoutePath.GENERATE_SEED_PHRASE }; }; const updateStoreAfterCreatePassword = (data: DataProps) => { @@ -180,7 +204,7 @@ const nextRoute: Record = { }, [RoutePath.ONBOARDING]: { nextPath: (data: DataProps) => getNextOnboardingRoute(data), - updateRedux: [], + updateRedux: [updateStoreRecoveryWallet], }, [RoutePath.SET_PASSCODE]: { nextPath: (data: DataProps) => getNextSetPasscodeRoute(data.store), @@ -194,12 +218,16 @@ const nextRoute: Record = { nextPath: () => getNextVerifySeedPhraseRoute(), updateRedux: [updateStoreAfterVerifySeedPhraseRoute, clearSeedPhraseCache], }, + [RoutePath.VERIFY_RECOVERY_SEED_PHRASE]: { + nextPath: () => getNextVerifySeedPhraseRoute(), + updateRedux: [], + }, [RoutePath.SSI_AGENT]: { nextPath: () => getNextCreateSSIAgentRoute(), updateRedux: [updateStoreAfterSetupSSI], }, [RoutePath.CREATE_PASSWORD]: { - nextPath: () => getNextCreatePasswordRoute(), + nextPath: (data: DataProps) => getNextCreatePasswordRoute(data), updateRedux: [updateStoreAfterCreatePassword], }, [RoutePath.CONNECTION_DETAILS]: { diff --git a/src/routes/paths.ts b/src/routes/paths.ts index 02626ef40..496c9e5d5 100644 --- a/src/routes/paths.ts +++ b/src/routes/paths.ts @@ -8,6 +8,7 @@ enum RoutePath { CREATE_PASSWORD = "/createpassword", SSI_AGENT = "/ssiagent", CONNECTION_DETAILS = "/connectiondetails", + VERIFY_RECOVERY_SEED_PHRASE = "/verifyrecoveryseedphrase", } enum TabsRoutePath { diff --git a/src/store/reducers/stateCache/stateCache.test.ts b/src/store/reducers/stateCache/stateCache.test.ts index 8238648e1..e15986ccd 100644 --- a/src/store/reducers/stateCache/stateCache.test.ts +++ b/src/store/reducers/stateCache/stateCache.test.ts @@ -57,6 +57,7 @@ describe("State Cache", () => { passwordIsSet: false, passwordIsSkipped: false, ssiAgentIsSet: false, + recoveryWalletProgress: false, }; const action = setAuthentication(authentication); const nextState = stateCacheSlice.reducer(initialState, action); diff --git a/src/store/reducers/stateCache/stateCache.ts b/src/store/reducers/stateCache/stateCache.ts index 160d14b19..201feca34 100644 --- a/src/store/reducers/stateCache/stateCache.ts +++ b/src/store/reducers/stateCache/stateCache.ts @@ -19,8 +19,9 @@ const initialState: StateCacheProps = { passcodeIsSet: false, seedPhraseIsSet: false, passwordIsSet: false, - passwordIsSkipped: true, + passwordIsSkipped: false, ssiAgentIsSet: false, + recoveryWalletProgress: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { diff --git a/src/store/reducers/stateCache/stateCache.types.ts b/src/store/reducers/stateCache/stateCache.types.ts index 7a15a075b..ddd27f32e 100644 --- a/src/store/reducers/stateCache/stateCache.types.ts +++ b/src/store/reducers/stateCache/stateCache.types.ts @@ -21,6 +21,7 @@ interface AuthenticationCacheProps { passwordIsSet: boolean; passwordIsSkipped: boolean; ssiAgentIsSet: boolean; + recoveryWalletProgress: boolean; } enum IncomingRequestType { CREDENTIAL_OFFER_RECEIVED = "credential-offer-received", diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 16c583a6e..29b8260e3 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -127,9 +127,7 @@ const App = () => { ); }; - const isPublicPage = PublicRoutes.includes( - window.location.pathname as RoutePath - ); + const isPublicPage = PublicRoutes.includes(currentRoute?.path as RoutePath); return ( diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index 7a76555f8..dc9b27c8c 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -315,6 +315,12 @@ const AppWrapper = (props: { children: ReactNode }) => { let userName: { userName: string } = { userName: "" }; const passcodeIsSet = await checkKeyStore(KeyStoreKeys.APP_PASSCODE); const seedPhraseIsSet = await checkKeyStore(KeyStoreKeys.SIGNIFY_BRAN); + const recoveryWalletProgress = await checkKeyStore( + KeyStoreKeys.RECOVERY_WALLET + ); + const passwordIsSkipped = await checkKeyStore( + KeyStoreKeys.PASSWORD_SKIPPED + ); const passwordIsSet = await checkKeyStore(KeyStoreKeys.APP_OP_PASSWORD); const keriaConnectUrlRecord = await Agent.agent.basicStorage.findById( @@ -368,8 +374,10 @@ const AppWrapper = (props: { children: ReactNode }) => { passcodeIsSet, seedPhraseIsSet, passwordIsSet, + passwordIsSkipped, ssiAgentIsSet: !!keriaConnectUrlRecord && !!keriaConnectUrlRecord.content.url, + recoveryWalletProgress, }) ); diff --git a/src/ui/components/SeedPhraseModule/SeedPhraseModule.scss b/src/ui/components/SeedPhraseModule/SeedPhraseModule.scss index 13370ff6e..50a40bef1 100644 --- a/src/ui/components/SeedPhraseModule/SeedPhraseModule.scss +++ b/src/ui/components/SeedPhraseModule/SeedPhraseModule.scss @@ -50,9 +50,26 @@ padding-inline: 0.625rem; font-size: 1rem; + &:focus-within { + border: 1px dashed var(--ion-color-secondary); + background-color: transparent; + } + + .word-input { + min-height: auto; + + .input-highlight.sc-ion-input-md { + display: none; + } + } + &.empty-word { border: 1px dashed var(--ion-color-dark-grey); background-color: transparent; + + &.error { + border-color: var(--ion-color-danger); + } } .index { @@ -72,6 +89,10 @@ font-size: 0.8rem; } + .word-input { + font-size: 0.8rem; + } + .index { margin-right: 0.125rem; } diff --git a/src/ui/components/SeedPhraseModule/SeedPhraseModule.tsx b/src/ui/components/SeedPhraseModule/SeedPhraseModule.tsx index f7f4775f1..f10ec3d15 100644 --- a/src/ui/components/SeedPhraseModule/SeedPhraseModule.tsx +++ b/src/ui/components/SeedPhraseModule/SeedPhraseModule.tsx @@ -1,82 +1,136 @@ -import { IonButton, IonChip, IonIcon } from "@ionic/react"; +import { IonButton, IonChip, IonIcon, IonInput } from "@ionic/react"; import { eyeOffOutline } from "ionicons/icons"; import { i18n } from "../../../i18n"; import "./SeedPhraseModule.scss"; -import { SeedPhraseModuleProps } from "./SeedPhraseModule.types"; +import { + SeedPhraseModuleProps, + SeedPhraseModuleRef, +} from "./SeedPhraseModule.types"; +import { forwardRef, useImperativeHandle, useRef } from "react"; +import { combineClassNames } from "../../utils/style"; -const SeedPhraseModule = ({ - testId, - seedPhrase, - hideSeedPhrase, - setHideSeedPhrase, - addSeedPhraseSelected, - removeSeedPhraseSelected, - emptyWord, - hideSeedNumber, -}: SeedPhraseModuleProps) => { - return ( -
+const SeedPhraseModule = forwardRef( + ( + { + testId, + seedPhrase, + hideSeedPhrase, + setHideSeedPhrase, + addSeedPhraseSelected, + removeSeedPhraseSelected, + emptyWord, + hideSeedNumber, + inputMode, + errorInputIndexs, + onInputChange, + onInputBlur, + onInputFocus, + }, + ref + ) => { + const seedInputs = useRef<(HTMLElement | null)[]>([]); + + useImperativeHandle(ref, () => ({ + focusInputByIndex: (index) => { + const input = seedInputs.current.at(index); + if (!input) return; + + (input as any).setFocus(); + }, + })); + + const getClassName = (word: string, index: number) => { + if (!inputMode) return; + + return combineClassNames("seed-chips", { + "empty-word": !word && index === seedPhrase.length - 1, + "empty-word error": + !!errorInputIndexs?.includes(index) || + (!word && index !== seedPhrase.length - 1), + }); + }; + + return (
- -

- {i18n.t("generateseedphrase.privacy.overlay.text")} -

- setHideSeedPhrase && setHideSeedPhrase(false)} +
- {i18n.t("generateseedphrase.privacy.overlay.button")} - -
-
- {seedPhrase.map((word, index) => { - return ( + +

+ {i18n.t("generateseedphrase.privacy.overlay.text")} +

+ setHideSeedPhrase && setHideSeedPhrase(false)} + > + {i18n.t("generateseedphrase.privacy.overlay.button")} + +
+
+ {seedPhrase.map((word, index) => { + return ( + { + if (removeSeedPhraseSelected) { + removeSeedPhraseSelected(index); + } else if (addSeedPhraseSelected) { + addSeedPhraseSelected(word); + } + }} + className={getClassName(word, index)} + > + {!hideSeedNumber && ( + + {index + 1}. + + )} + {inputMode ? ( + (seedInputs.current[index] = ref)} + value={word} + onIonInput={(e) => { + onInputChange?.(e.target.value as string, index); + }} + onIonFocus={() => onInputFocus?.(index)} + onIonBlur={() => onInputBlur?.(index)} + name={`word-input-${index}`} + id={`word-input-${index}`} + data-testid={`word-input-${index}`} + /> + ) : ( + {word} + )} + + ); + })} + {emptyWord && ( { - if (removeSeedPhraseSelected) { - removeSeedPhraseSelected(index); - } else if (addSeedPhraseSelected) { - addSeedPhraseSelected(word); - } - }} + className="empty-word" + data-testid={`empty-word-${seedPhrase.length + 1}`} > - {!hideSeedNumber && ( - - {index + 1}. - - )} - {word} + {seedPhrase.length + 1}. - ); - })} - {emptyWord && ( - - {seedPhrase.length + 1}. - - )} + )} +
-
- ); -}; + ); + } +); export { SeedPhraseModule }; diff --git a/src/ui/components/SeedPhraseModule/SeedPhraseModule.types.ts b/src/ui/components/SeedPhraseModule/SeedPhraseModule.types.ts index d856d3184..8280e82d4 100644 --- a/src/ui/components/SeedPhraseModule/SeedPhraseModule.types.ts +++ b/src/ui/components/SeedPhraseModule/SeedPhraseModule.types.ts @@ -7,6 +7,15 @@ interface SeedPhraseModuleProps { removeSeedPhraseSelected?: (index: number) => void; emptyWord?: boolean; hideSeedNumber?: boolean; + inputMode?: boolean; + onInputChange?: (value: string, index: number) => void; + onInputFocus?: (index: number) => void; + onInputBlur?: (index: number) => void; + errorInputIndexs?: number[]; } -export type { SeedPhraseModuleProps }; +interface SeedPhraseModuleRef { + focusInputByIndex: (index: number) => void; +} + +export type { SeedPhraseModuleProps, SeedPhraseModuleRef }; diff --git a/src/ui/pages/CreatePassword/CreatePassword.tsx b/src/ui/pages/CreatePassword/CreatePassword.tsx index f94ac1582..8cea93518 100644 --- a/src/ui/pages/CreatePassword/CreatePassword.tsx +++ b/src/ui/pages/CreatePassword/CreatePassword.tsx @@ -72,6 +72,8 @@ const CreatePassword = () => { }) ); } + } else { + await SecureStorage.set(KeyStoreKeys.PASSWORD_SKIPPED, String(true)); } const { nextPath, updateRedux } = getNextRoute(RoutePath.CREATE_PASSWORD, { diff --git a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx index d44d548a1..497db0819 100644 --- a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx +++ b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx @@ -13,15 +13,17 @@ import { setCurrentOperation } from "../../../store/reducers/stateCache"; import { OperationType } from "../../globals/types"; import { setBootUrl, setConnectUrl } from "../../../store/reducers/ssiAgent"; import { RoutePath } from "../../../routes"; -import { Agent } from "../../../core/agent/agent"; +import { KeyStoreKeys } from "../../../core/storage"; const bootAndConnectMock = jest.fn((...args: any) => Promise.resolve()); +const recoverKeriaAgentMock = jest.fn(); jest.mock("../../../core/agent/agent", () => ({ Agent: { ...jest.requireActual("../../../core/agent/agent"), agent: { bootAndConnect: (...args: any) => bootAndConnectMock(...args), + recoverKeriaAgent: (...args: any) => recoverKeriaAgentMock(...args), }, }, })); @@ -67,6 +69,15 @@ jest.mock("../../components/CustomInput", () => ({ }, })); +const secureStorageDeleteFunc = jest.fn(); + +jest.mock("../../../core/storage", () => ({ + ...jest.requireActual("../../../core/storage"), + SecureStorage: { + delete: (...args: any) => secureStorageDeleteFunc(...args), + }, +})); + describe("SSI agent page", () => { const mockStore = configureStore(); const dispatchMock = jest.fn(); @@ -75,6 +86,14 @@ describe("SSI agent page", () => { bootUrl: undefined, connectUrl: undefined, }, + stateCache: { + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + recoveryWalletProgress: false, + }, + }, }; const storeMocked = { @@ -156,6 +175,14 @@ describe("SSI agent page", () => { bootUrl: "11111", connectUrl: undefined, }, + stateCache: { + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + recoveryWalletProgress: false, + }, + }, }; const storeMocked = { @@ -185,6 +212,14 @@ describe("SSI agent page", () => { bootUrl: undefined, connectUrl: "11111", }, + stateCache: { + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + recoveryWalletProgress: false, + }, + }, }; const storeMocked = { @@ -216,6 +251,14 @@ describe("SSI agent page", () => { bootUrl: undefined, connectUrl: "https://connectUrl.com/", }, + stateCache: { + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + recoveryWalletProgress: false, + }, + }, }; const storeMocked = { @@ -247,6 +290,14 @@ describe("SSI agent page", () => { bootUrl: "11111", connectUrl: "11111", }, + stateCache: { + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + recoveryWalletProgress: false, + }, + }, }; const storeMocked = { @@ -320,3 +371,100 @@ describe("SSI agent page", () => { }); }); }); + +describe("SSI agent page: recovery mode", () => { + const mockStore = configureStore(); + const dispatchMock = jest.fn(); + const initialState = { + ssiAgentCache: { + bootUrl: undefined, + connectUrl: undefined, + }, + stateCache: { + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + recoveryWalletProgress: true, + }, + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + test("Renders ssi agent page", () => { + const { getByText, getByTestId, queryByTestId } = render( + + + + ); + + expect(getByText(ENG_Trans.ssiagent.title)).toBeVisible(); + expect(getByText(ENG_Trans.ssiagent.verifydescription)).toBeVisible(); + expect(getByText(ENG_Trans.ssiagent.button.info)).toBeVisible(); + expect(getByText(ENG_Trans.ssiagent.button.validate)).toBeVisible(); + expect( + getByText(ENG_Trans.ssiagent.button.validate).getAttribute("disabled") + ).toBe("true"); + + expect(queryByTestId("boot-url-input")).toBe(null); + expect(getByTestId("connect-url-input")).toBeVisible(); + }); + + test("Connect and boot success", async () => { + const mockStore = configureStore(); + const initialState = { + stateCache: { + authentication: { + passcodeIsSet: true, + seedPhraseIsSet: true, + passwordIsSet: true, + passwordIsSkipped: true, + loggedIn: false, + userName: "", + time: 0, + ssiAgentIsSet: false, + recoveryWalletProgress: true, + }, + }, + ssiAgentCache: { + bootUrl: + "https://dev.keria-boot.cf-keripy.metadata.dev.cf-deployments.org", + connectUrl: + "https://dev.keria.cf-keripy.metadata.dev.cf-deployments.org", + }, + seedPhraseCache: { + seedPhrase: "mock-seed", + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + const history = createMemoryHistory(); + history.push(RoutePath.SSI_AGENT); + + const { getByTestId } = render( + + + + + + ); + + act(() => { + fireEvent.click(getByTestId("primary-button-create-ssi-agent")); + }); + + await waitFor(() => { + expect(secureStorageDeleteFunc).toBeCalledWith( + KeyStoreKeys.RECOVERY_WALLET + ); + }); + }); +}); diff --git a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx index efd4f8351..52aac2291 100644 --- a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx +++ b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx @@ -33,8 +33,11 @@ import { TermsModal } from "../../components/TermsModal"; import { Agent } from "../../../core/agent/agent"; import { ScrollablePageLayout } from "../../components/layout/ScrollablePageLayout"; import { ConfigurationService } from "../../../core/configuration"; +import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; +import { getSeedPhraseCache } from "../../../store/reducers/seedPhraseCache"; const SSI_URLS_EMPTY = "SSI url is empty"; +const SEED_PHRASE_EMPTY = "Invalid seed phrase"; const InputError = ({ showError, @@ -53,8 +56,9 @@ const InputError = ({ const CreateSSIAgent = () => { const pageId = "create-ssi-agent"; const ssiAgent = useAppSelector(getSSIAgent); - + const seedPhraseCache = useAppSelector(getSeedPhraseCache); const stateCache = useAppSelector(getStateCache); + const ionRouter = useAppIonRouter(); const dispatch = useAppDispatch(); const [connectUrlInputTouched, setConnectUrlTouched] = useState(false); @@ -63,6 +67,9 @@ const CreateSSIAgent = () => { const [loading, setLoading] = useState(false); const [hasMismatchError, setHasMismatchError] = useState(false); const [isInvalidBootUrl, setIsInvalidBootUrl] = useState(false); + const [isInvalidConnectUrl, setInvalidConnectUrl] = useState(false); + + const isRecoveryMode = stateCache.authentication.recoveryWalletProgress; useEffect(() => { if (!ssiAgent.bootUrl && !ssiAgent.connectUrl) { @@ -84,7 +91,9 @@ const CreateSSIAgent = () => { }; const validBootUrl = useMemo(() => { - return ssiAgent.bootUrl && isValidHttpUrl(ssiAgent.bootUrl); + return ( + isRecoveryMode || (ssiAgent.bootUrl && isValidHttpUrl(ssiAgent.bootUrl)) + ); }, [ssiAgent]); const validConnectUrl = useMemo(() => { @@ -92,6 +101,7 @@ const CreateSSIAgent = () => { }, [ssiAgent]); const displayBootUrlError = + !isRecoveryMode && bootUrlInputTouched && ssiAgent.bootUrl && !isValidHttpUrl(ssiAgent.bootUrl); @@ -107,7 +117,62 @@ const CreateSSIAgent = () => { dispatch(clearSSIAgent()); }; - const handleValidate = async () => { + const handleRecoveryWallet = async () => { + setLoading(true); + try { + if (!ssiAgent.connectUrl) { + throw new Error(SSI_URLS_EMPTY); + } + + if (!seedPhraseCache.seedPhrase) { + throw new Error(SEED_PHRASE_EMPTY); + } + + await Agent.agent.recoverKeriaAgent( + seedPhraseCache.seedPhrase.split(" "), + ssiAgent.connectUrl + ); + + const { nextPath, updateRedux } = getNextRoute(RoutePath.SSI_AGENT, { + store: { stateCache }, + }); + + updateReduxState( + nextPath.pathname, + { + store: { stateCache }, + }, + dispatch, + updateRedux + ); + + SecureStorage.delete(KeyStoreKeys.RECOVERY_WALLET); + + ionRouter.push(nextPath.pathname, "forward", "push"); + handleClearState(); + } catch (e) { + const errorMessage = (e as Error).message; + + if ( + [SSI_URLS_EMPTY, SEED_PHRASE_EMPTY, Agent.INVALID_MNEMONIC].includes( + errorMessage + ) + ) { + return; + } + + if (Agent.KERIA_NOT_BOOTED === errorMessage) { + setHasMismatchError(true); + return; + } + + setInvalidConnectUrl(true); + } finally { + setLoading(false); + } + }; + + const handleCreateSSI = async () => { setLoading(true); try { if (!ssiAgent.bootUrl || !ssiAgent.connectUrl) { @@ -115,7 +180,7 @@ const CreateSSIAgent = () => { } await Agent.agent.bootAndConnect({ - bootUrl: ssiAgent.bootUrl, + bootUrl: ssiAgent.bootUrl || "", url: ssiAgent.connectUrl, }); @@ -148,6 +213,14 @@ const CreateSSIAgent = () => { } }; + const handleValidate = () => { + if (isRecoveryMode) { + handleRecoveryWallet(); + } else { + handleCreateSSI(); + } + }; + const scanBootUrl = (event: ReactMouseEvent) => { event.stopPropagation(); dispatch(setCurrentOperation(OperationType.SCAN_SSI_BOOT_URL)); @@ -168,6 +241,17 @@ const CreateSSIAgent = () => { return result; }; + const handleChangeConnectUrl = (connectionUrl: string) => { + setInvalidConnectUrl(false); + setHasMismatchError(false); + dispatch(setConnectUrl(connectionUrl)); + }; + + const handleChangeBootUrl = (bootUrl: string) => { + setIsInvalidBootUrl(false); + dispatch(setBootUrl(bootUrl)); + }; + return ( <> { className="page-paragraph" data-testid={`${pageId}-top-paragraph`} > - {i18n.t("ssiagent.description")} + {i18n.t( + isRecoveryMode + ? "ssiagent.verifydescription" + : "ssiagent.description" + )}

{ {i18n.t("ssiagent.button.info")}
- { - setIsInvalidBootUrl(false); - dispatch(setBootUrl(bootUrl)); - }} - value={ssiAgent.bootUrl || ""} - onChangeFocus={(result) => { - setTouchedBootUrlInput(); + {!isRecoveryMode && ( + <> + { + setTouchedBootUrlInput(); - if (!result && ssiAgent.bootUrl) { - dispatch(setBootUrl(removeLastSlash(ssiAgent.bootUrl.trim()))); - } - }} - error={!!displayBootUrlError || isInvalidBootUrl} - /> - + if (!result && ssiAgent.bootUrl) { + dispatch( + setBootUrl(removeLastSlash(ssiAgent.bootUrl.trim())) + ); + } + }} + error={!!displayBootUrlError || isInvalidBootUrl} + /> + + + )} { - setHasMismatchError(false); - dispatch(setConnectUrl(connectionUrl)); - }} + onChangeInput={handleChangeConnectUrl} onChangeFocus={(result) => { setTouchedConnectUrlInput(); @@ -251,14 +340,18 @@ const CreateSSIAgent = () => { } }} value={ssiAgent.connectUrl || ""} - error={!!displayConnectUrlError || hasMismatchError} + error={ + !!displayConnectUrlError || hasMismatchError || isInvalidConnectUrl + } /> { // @TODO - foconnor: This should be op: OperationType when available (non optional) const handleNavigation = (op?: string) => { - if (op) { - // @TODO - sdisalvo: Remove this condition and default to dispatch when the restore route is ready - return; - } const data: DataProps = { store: { stateCache }, + state: { + recoveryWalletProgress: !!op, + }, }; const { nextPath, updateRedux } = getNextRoute(RoutePath.ONBOARDING, data); updateReduxState(nextPath.pathname, data, dispatch, updateRedux); + + if (op) { + SecureStorage.set(KeyStoreKeys.RECOVERY_WALLET, String(!!op)); + } + history.push({ pathname: nextPath.pathname, state: data.state, diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.scss b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.scss new file mode 100644 index 000000000..5b86ce413 --- /dev/null +++ b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.scss @@ -0,0 +1,93 @@ +.verify-recovery-seed-phrase { + --background: var(--ion-color-light); + + .seed-phrase-module:nth-of-type(1) { + background: var(--ion-color-light-grey); + } + + .page-header { + ion-toolbar { + --background: transparent; + } + } + + .page-title { + font-size: 1.5rem; + line-height: 1.75rem; + margin-top: 0.5rem; + font-weight: 700; + } + + .content-container { + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: calc(100vh - (5.25rem + var(--ion-safe-area-top))); + + .page-content { + display: flex; + flex-direction: column; + + & > * { + flex: 0 1 auto; + } + } + } + + .paragraph-top { + margin: 0.75rem 0 1rem; + } + + .suggest-error { + font-size: 1rem; + line-height: 1.115rem; + color: var(--ion-color-primary); + text-align: center; + font-weight: 400; + } + + .suggestion-title { + margin: 1.5rem 0 0.75rem; + font-size: 1rem; + font-weight: 600; + line-height: 1.5rem; + } + + .seed-phrase-module:nth-of-type(2) { + border: none; + } + + .clear-button { + display: flex; + width: fit-content; + margin: 1.5rem auto 1rem; + } + + @media screen and (min-width: 250px) and (max-width: 370px) { + .responsive-page-layout { + padding: 0 0.9rem; + + .responsive-page-content { + & > * { + margin: 0; + } + + & > .paragraph-top { + margin: 0.2rem 0; + } + } + } + + .suggest-error { + font-size: 0.8rem; + } + + .clear-button { + margin-top: 1rem; + } + + .suggestion-title { + margin-top: 1rem; + } + } +} diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx new file mode 100644 index 000000000..78710611e --- /dev/null +++ b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx @@ -0,0 +1,368 @@ +import { IonReactMemoryRouter } from "@ionic/react-router"; +import { fireEvent, render, waitFor } from "@testing-library/react"; +import { createMemoryHistory } from "history"; +import { act } from "react-dom/test-utils"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import ENG_Trans from "../../../locales/en/en.json"; +import { RoutePath } from "../../../routes"; +import { VerifyRecoverySeedPhrase } from "./VerifyRecoverySeedPhrase"; +import { setSeedPhraseCache } from "../../../store/reducers/seedPhraseCache"; + +const SEED_PHRASE_LENGTH = 18; + +const secureStorageGetFunc = jest.fn(); +const secureStorageSetFunc = jest.fn(); +const secureStorageDeleteFunc = jest.fn(); +const verifySeedPhraseFnc = jest.fn(); + +jest.mock("../../../core/agent/agent", () => ({ + Agent: { + agent: { + isMnemonicValid: () => verifySeedPhraseFnc(), + }, + }, +})); + +jest.mock("../../../core/storage", () => ({ + ...jest.requireActual("../../../core/storage"), + SecureStorage: { + get: (...args: any) => secureStorageGetFunc(...args), + set: (...args: any) => secureStorageSetFunc(...args), + delete: (...args: any) => secureStorageDeleteFunc(...args), + }, +})); + +jest.mock("@ionic/react", () => ({ + ...jest.requireActual("@ionic/react"), + IonInput: (props: any) => { + return ( + props.onIonBlur(e)} + onFocus={(e) => props.onIonFocus(e)} + onChange={(e) => props.onIonInput?.(e)} + /> + ); + }, +})); + +describe("Verify Recovery Seed Phrase", () => { + const mockStore = configureStore(); + const dispatchMock = jest.fn(); + const initialState = { + stateCache: { + authentication: { + loggedIn: true, + time: Date.now(), + passcodeIsSet: true, + recoveryWalletProgress: true, + }, + }, + }; + + const storeMocked = { + ...mockStore(initialState), + dispatch: dispatchMock, + }; + + test("Render screen", () => { + const { getByText, getByTestId } = render( + + + + ); + + expect(getByText(ENG_Trans.verifyrecoveryseedphrase.title)).toBeVisible(); + expect( + getByText(ENG_Trans.verifyrecoveryseedphrase.paragraph.top) + ).toBeVisible(); + expect( + getByText( + ENG_Trans.verifyrecoveryseedphrase.button.continue + ).getAttribute("disabled") + ).toBe("true"); + expect(getByTestId("word-input-0")).toBeVisible(); + }); + + test("Render suggest seed phrase", async () => { + const { getByText, getByTestId } = render( + + + + ); + + expect(getByText(ENG_Trans.verifyrecoveryseedphrase.title)).toBeVisible(); + + const firstInput = getByTestId("word-input-0"); + act(() => { + fireEvent.focus(firstInput); + fireEvent.change(firstInput, { + target: { value: "a" }, + }); + }); + + expect( + getByText(ENG_Trans.verifyrecoveryseedphrase.suggestions.title) + ).toBeVisible(); + expect(getByText("abandon")).toBeVisible(); + expect(getByTestId("word-input-1")).toBeVisible(); + + act(() => { + fireEvent.click(getByText("abandon")); + }); + + await waitFor(() => { + expect((firstInput as HTMLInputElement).value).toEqual("abandon"); + }); + }); + + test("Render/clear word should match suggestion error", async () => { + const { getByText, getByTestId, queryByTestId } = render( + + + + ); + + expect(getByText(ENG_Trans.verifyrecoveryseedphrase.title)).toBeVisible(); + + const firstInput = getByTestId("word-input-0"); + act(() => { + fireEvent.focus(firstInput); + fireEvent.change(firstInput, { + target: { value: "a" }, + }); + }); + + expect( + getByText(ENG_Trans.verifyrecoveryseedphrase.suggestions.title) + ).toBeVisible(); + + act(() => { + fireEvent.blur(firstInput); + }); + + await waitFor(() => { + expect(getByTestId("no-suggest-error")).toBeVisible(); + }); + + act(() => { + fireEvent.focus(firstInput); + fireEvent.change(firstInput, { + target: { value: "abandon" }, + }); + }); + + expect( + getByText(ENG_Trans.verifyrecoveryseedphrase.suggestions.title) + ).toBeVisible(); + + act(() => { + fireEvent.blur(firstInput); + }); + + await waitFor(() => { + expect(queryByTestId("no-suggest-error")).toBe(null); + }); + }); + + test("Fill all seed", async () => { + const history = createMemoryHistory(); + history.push(RoutePath.VERIFY_RECOVERY_SEED_PHRASE); + + verifySeedPhraseFnc.mockImplementation(() => { + return Promise.resolve(true); + }); + + const { getByText, getByTestId } = render( + + + + + + ); + + expect(getByText(ENG_Trans.verifyrecoveryseedphrase.title)).toBeVisible(); + for (let i = 0; i < SEED_PHRASE_LENGTH; i++) { + act(() => { + const input = getByTestId(`word-input-${i}`); + fireEvent.focus(input); + fireEvent.change(input, { + target: { value: "a" }, + }); + }); + + await waitFor(() => { + expect(getByText("abandon")).toBeVisible(); + }); + + act(() => { + fireEvent.click(getByText("abandon")); + }); + + if (i < SEED_PHRASE_LENGTH - 1) { + await waitFor(() => { + expect(getByTestId(`word-input-${i}`)).toBeVisible(); + }); + } + } + + expect( + getByText( + ENG_Trans.verifyrecoveryseedphrase.button.continue + ).getAttribute("disabled") + ).toBe("false"); + + act(() => { + fireEvent.click( + getByText(ENG_Trans.verifyrecoveryseedphrase.button.continue) + ); + }); + + await waitFor(() => { + expect(dispatchMock).toBeCalledWith( + setSeedPhraseCache({ + seedPhrase: + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon", + bran: "", + }) + ); + }); + }); + + test("Lock when try max attempt", async () => { + const history = createMemoryHistory(); + history.push(RoutePath.VERIFY_RECOVERY_SEED_PHRASE); + + const { getByText, getByTestId } = render( + + + + + + ); + + expect(getByText(ENG_Trans.verifyrecoveryseedphrase.title)).toBeVisible(); + for (let i = 0; i < SEED_PHRASE_LENGTH; i++) { + act(() => { + const input = getByTestId(`word-input-${i}`); + fireEvent.focus(input); + fireEvent.change(input, { + target: { value: "a" }, + }); + }); + + await waitFor(() => { + expect(getByText("abandon")).toBeVisible(); + }); + + act(() => { + fireEvent.click(getByText("abandon")); + }); + + if (i < SEED_PHRASE_LENGTH - 1) { + await waitFor(() => { + expect(getByTestId(`word-input-${i}`)).toBeVisible(); + }); + } + } + + expect( + getByText( + ENG_Trans.verifyrecoveryseedphrase.button.continue + ).getAttribute("disabled") + ).toBe("false"); + + verifySeedPhraseFnc.mockImplementation(() => { + return Promise.resolve(false); + }); + + for (let i = 0; i < 5; i++) { + act(() => { + fireEvent.click( + getByText(ENG_Trans.verifyrecoveryseedphrase.button.continue) + ); + }); + + await waitFor(() => { + expect( + getByText(ENG_Trans.verifyrecoveryseedphrase.alert.fail.text) + ).toBeVisible(); + }); + + act(() => { + fireEvent.click( + getByText( + ENG_Trans.verifyrecoveryseedphrase.alert.fail.button.confirm + ) + ); + }); + } + + await waitFor(() => { + expect( + getByText(ENG_Trans.verifyrecoveryseedphrase.alert.toomanyattempts.text) + ).toBeVisible(); + expect( + getByText(ENG_Trans.verifyrecoveryseedphrase.button.lock) + ).toBeVisible(); + }); + }); + + test("Lock when try max attempt after reload page", async () => { + const history = createMemoryHistory(); + history.push(RoutePath.VERIFY_RECOVERY_SEED_PHRASE); + + secureStorageGetFunc.mockImplementation(() => { + return Promise.resolve(Date.now() - 1000); + }); + + const { getByText } = render( + + + + + + ); + + await waitFor(() => { + expect( + getByText(ENG_Trans.verifyrecoveryseedphrase.button.lock) + ).toBeVisible(); + }); + }); + + test("Remove last lock time when duration greater than 10 minutes", async () => { + const history = createMemoryHistory(); + history.push(RoutePath.VERIFY_RECOVERY_SEED_PHRASE); + + secureStorageGetFunc.mockImplementation(() => { + return Promise.resolve(Date.now() - 11 * 60 * 1000); + }); + + render( + + + + + + ); + + await waitFor(() => { + expect(secureStorageDeleteFunc).toBeCalled(); + }); + }); +}); diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.tsx b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.tsx new file mode 100644 index 000000000..22319472f --- /dev/null +++ b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.tsx @@ -0,0 +1,484 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { useHistory } from "react-router-dom"; +import { closeOutline } from "ionicons/icons"; +import { wordlists } from "bip39"; +import { IonButton, IonIcon } from "@ionic/react"; +import { i18n } from "../../../i18n"; +import { RoutePath } from "../../../routes"; +import { useAppDispatch, useAppSelector } from "../../../store/hooks"; +import { Alert as AlertFail } from "../../components/Alert"; +import "./VerifyRecoverySeedPhrase.scss"; +import { getNextRoute } from "../../../routes/nextRoute"; +import { updateReduxState } from "../../../store/utils"; +import { getStateCache } from "../../../store/reducers/stateCache"; +import { getBackRoute } from "../../../routes/backRoute"; +import { DataProps } from "../../../routes/nextRoute/nextRoute.types"; +import { PageHeader } from "../../components/PageHeader"; +import { PageFooter } from "../../components/PageFooter"; +import { SeedPhraseModule } from "../../components/SeedPhraseModule"; +import { useAppIonRouter } from "../../hooks"; +import { ScrollablePageLayout } from "../../components/layout/ScrollablePageLayout"; +import { SeedPhraseModuleRef } from "../../components/SeedPhraseModule/SeedPhraseModule.types"; +import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; +import { setSeedPhraseCache } from "../../../store/reducers/seedPhraseCache"; +import { Agent } from "../../../core/agent/agent"; +import { SeedPhraseInfo } from "./VerifyRecoverySeedPhrase.types"; + +const SEED_PHRASE_LENGTH = 18; +const SUGGEST_SEED_PHRASE_LENGTH = 4; +const MAX_ATTEMPT_FAIL = 5; +const LOCK_TIME = 60000; +const RESET_ATTEMPT_TIME = 600000; +const SELECT_WORD_LIST = wordlists.english; +const INVALID_SEED_PHRASE = "Invalid seed phrase"; + +const VerifyRecoverySeedPhrase = () => { + const pageId = "verify-recovery-seed-phrase"; + const history = useHistory(); + const dispatch = useAppDispatch(); + const stateCache = useAppSelector(getStateCache); + + const [alertIsOpen, setAlertIsOpen] = useState(false); + const [clearAlertOpen, setClearAlertOpen] = useState(false); + const [alertManyAttempOpen, setAlertManyAttempOpen] = useState(false); + const ionRouter = useAppIonRouter(); + + const [seedPhraseInfo, setSeedPhraseInfo] = useState([ + { + value: "", + suggestions: [], + }, + ]); + + const [failAttempt, setFailAttempt] = useState(0); + const [lastLockTime, setLastLockTime] = useState(null); + const [lockFailAttempt, setLockFailAttempt] = useState(false); + const [lastFocusIndex, setLastFocusIndex] = useState(null); + const [isTyping, setIsTyping] = useState(false); + + const seedPhraseRef = useRef(null); + + const seedPhrase = seedPhraseInfo.map((item) => item.value); + const suggestSeedPhrase = + (isTyping && + seedPhraseInfo.find((_, index) => index === lastFocusIndex) + ?.suggestions) || + []; + const errorInputIndex = seedPhraseInfo.reduce((result, nextItem, index) => { + if (nextItem.suggestions.includes(nextItem.value)) return result; + + if (nextItem.value && nextItem.suggestions.length === 0) { + result.push(index); + } + + if ( + nextItem.value && + nextItem.suggestions.length && + (index !== lastFocusIndex || !isTyping) + ) { + result.push(index); + } + + if (!nextItem.value && index !== seedPhrase.length - 1) { + result.push(index); + } + + return result; + }, [] as number[]); + + const removeLockAttempt = () => { + SecureStorage.delete(KeyStoreKeys.RECOVERY_WALLET_LAST_FAIL_ATTEMPT_TIME); + setFailAttempt(0); + setLockFailAttempt(false); + }; + + const getLockDuration = useCallback(() => { + return Date.now() - (!lastLockTime ? 0 : lastLockTime.getTime()); + }, [lastLockTime]); + + useEffect(() => { + async function getFailAttemptInfo() { + try { + const lastFailedTime = await SecureStorage.get( + KeyStoreKeys.RECOVERY_WALLET_LAST_FAIL_ATTEMPT_TIME + ); + if (!lastFailedTime) return; + + const lockDuration = Date.now() - Number(lastFailedTime); + + if (lockDuration >= RESET_ATTEMPT_TIME) { + removeLockAttempt(); + return; + } + + setFailAttempt(MAX_ATTEMPT_FAIL); + setLastLockTime(new Date(Number(lastFailedTime))); + } catch (e) { + // TODO: handle error + } + } + + getFailAttemptInfo(); + }, []); + + // Reset attempt numbers after 10 minutes from last lock period + useEffect(() => { + if (!lastLockTime) return; + + const resetAttemptTime = RESET_ATTEMPT_TIME - getLockDuration(); + + if (resetAttemptTime <= 0) return; + + const timerId = setTimeout(() => { + removeLockAttempt(); + }, resetAttemptTime); + + return () => { + clearTimeout(timerId); + }; + }, [lastLockTime]); + + useEffect(() => { + if (!lastLockTime) return; + + const lockTime = LOCK_TIME - getLockDuration(); + + if (lockTime <= 0) { + setLockFailAttempt(false); + return; + } + + setLockFailAttempt(true); + setFailAttempt(MAX_ATTEMPT_FAIL); + + const timerId = setTimeout(() => { + setLockFailAttempt(false); + }, lockTime); + + return () => { + clearTimeout(timerId); + }; + }, [lastLockTime]); + + const handleClearState = () => { + setSeedPhraseInfo([ + { + value: "", + suggestions: [], + }, + ]); + setLastFocusIndex(null); + setAlertIsOpen(false); + seedPhraseRef.current?.focusInputByIndex(0); + }; + + const renderSuggestSeedPhrase = (inputWord: string) => { + inputWord = inputWord.toLowerCase().trim(); + + if (!inputWord) { + return []; + } + + const suggestionWords: string[] = []; + for (let index = 0; index < SELECT_WORD_LIST.length; index++) { + const word = SELECT_WORD_LIST[index]; + + if (word.startsWith(inputWord)) { + suggestionWords.push(word); + } + + if (suggestionWords.length >= SUGGEST_SEED_PHRASE_LENGTH) { + break; + } + } + + return suggestionWords; + }; + + const handleUpdateSeedPhrase = (value: string, index: number) => { + const currentValue = [...seedPhraseInfo]; + + const suggestions = renderSuggestSeedPhrase(value); + + currentValue[index] = { + value, + suggestions: suggestions, + }; + + if (!value && index === currentValue.length - 2) { + currentValue.splice(currentValue.length - 2, 1); + } else if ( + index + 1 === currentValue.length && + index + 1 < SEED_PHRASE_LENGTH + ) { + currentValue.push({ + value: "", + suggestions: [], + }); + } + + setSeedPhraseInfo(currentValue); + + return currentValue; + }; + + const addSeedPhraseSelected = (word: string) => { + if (lastFocusIndex === null) return; + + const newValue = handleUpdateSeedPhrase(word, lastFocusIndex); + + if ( + lastFocusIndex !== SEED_PHRASE_LENGTH - 1 && + filledSeedPhrase.length !== SEED_PHRASE_LENGTH + ) { + seedPhraseRef.current?.focusInputByIndex(newValue.length - 1); + } + }; + + const handleContinue = async () => { + try { + const seedPhraseText = seedPhrase.join(" "); + const verifyResult = await Agent.agent.isMnemonicValid(seedPhraseText); + + if (!verifyResult) { + throw new Error(INVALID_SEED_PHRASE); + } + + dispatch( + setSeedPhraseCache({ + seedPhrase: seedPhraseText, + bran: "", + }) + ); + SecureStorage.delete(KeyStoreKeys.RECOVERY_WALLET_LAST_FAIL_ATTEMPT_TIME); + setFailAttempt(0); + + handleNavigate(); + } catch (e) { + setFailAttempt((value) => value + 1); + + if (failAttempt + 1 >= MAX_ATTEMPT_FAIL) { + const now = new Date(); + SecureStorage.set( + KeyStoreKeys.RECOVERY_WALLET_LAST_FAIL_ATTEMPT_TIME, + String(now.getTime()) + ); + setLastLockTime(now); + setAlertManyAttempOpen(true); + setLockFailAttempt(true); + } else { + setAlertIsOpen(true); + } + } + }; + + const handleNavigate = () => { + const data: DataProps = { + store: { stateCache }, + state: { + currentOperation: stateCache.currentOperation, + }, + }; + + const { nextPath, updateRedux } = getNextRoute( + RoutePath.VERIFY_RECOVERY_SEED_PHRASE, + data + ); + + updateReduxState(nextPath.pathname, data, dispatch, updateRedux); + handleClearState(); + + ionRouter.push(nextPath.pathname, "root", "replace"); + }; + + const handleBack = () => { + handleClearState(); + const { backPath, updateRedux } = getBackRoute( + RoutePath.VERIFY_RECOVERY_SEED_PHRASE, + { + store: { stateCache }, + } + ); + updateReduxState( + backPath.pathname, + { store: { stateCache } }, + dispatch, + updateRedux + ); + history.push({ + pathname: backPath.pathname, + }); + }; + + const closeFailAlert = () => { + setAlertIsOpen(false); + }; + + const closeClearAlert = () => { + setClearAlertOpen(false); + }; + + const onFocusInput = (index: number) => { + setLastFocusIndex(index); + setIsTyping(true); + const word = seedPhrase[index]; + renderSuggestSeedPhrase(word); + }; + + const checkMatchWithSuggestion = (selectWord: string | undefined) => { + return SELECT_WORD_LIST.some( + (word) => selectWord?.toLowerCase().trim() === word + ); + }; + + const onBlurInput = (index: number) => { + setIsTyping(false); + }; + + const verifyButtonLabel = `${i18n.t( + lockFailAttempt + ? "verifyrecoveryseedphrase.button.lock" + : "verifyrecoveryseedphrase.button.continue" + )}`; + + const displaySuggestionError = errorInputIndex.length > 0; + const filledSeedPhrase = seedPhrase.filter((item) => !!item); + const isMatchAllSuggestion = filledSeedPhrase.every((seedPhrase) => + checkMatchWithSuggestion(seedPhrase) + ); + + return ( + <> + { + handleClearState(); + handleBack(); + }} + currentPath={RoutePath.VERIFY_RECOVERY_SEED_PHRASE} + progressBar={true} + progressBarValue={0.75} + progressBarBuffer={1} + /> + } + > +
+
+

+ {i18n.t("verifyrecoveryseedphrase.title")} +

+

+ {i18n.t("verifyrecoveryseedphrase.paragraph.top")} +

+ + {(suggestSeedPhrase.length > 0 || displaySuggestionError) && ( +

+ {i18n.t("verifyrecoveryseedphrase.suggestions.title")} +

+ )} + {suggestSeedPhrase.length > 0 && ( + + )} + {displaySuggestionError && ( +

+ {i18n.t("verifyrecoveryseedphrase.suggestions.error")} +

+ )} + {seedPhrase.filter((item) => !!item).length > 0 && ( + setClearAlertOpen(true)} + fill="outline" + data-testid="verify-clear-button" + className="clear-button secondary-button" + > + + {i18n.t("verifyrecoveryseedphrase.button.clear")} + + )} +
+ handleContinue()} + primaryButtonDisabled={ + filledSeedPhrase.length < SEED_PHRASE_LENGTH || + lockFailAttempt || + displaySuggestionError || + !isMatchAllSuggestion + } + /> +
+
+ + + + + ); +}; + +export { VerifyRecoverySeedPhrase }; diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.types.ts b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.types.ts new file mode 100644 index 000000000..620609c97 --- /dev/null +++ b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.types.ts @@ -0,0 +1,6 @@ +interface SeedPhraseInfo { + value: string; + suggestions: string[]; +} + +export type { SeedPhraseInfo }; diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/index.ts b/src/ui/pages/VerifyRecoverySeedPhrase/index.ts new file mode 100644 index 000000000..4e21faa7f --- /dev/null +++ b/src/ui/pages/VerifyRecoverySeedPhrase/index.ts @@ -0,0 +1 @@ +export * from "./VerifyRecoverySeedPhrase"; diff --git a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx index 5d9a293f9..69641e8bf 100644 --- a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx +++ b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx @@ -298,7 +298,7 @@ describe("Verify Seed Phrase Page", () => { fireEvent.click(backButton); }); - expect(continueButton.disabled).toBe(true); + expect(dispatchMock).toBeCalledTimes(2); }); test("The user can remove words from the Seed Phrase", async () => { From 20466249fb069523d03984699c80d7765f264e67 Mon Sep 17 00:00:00 2001 From: Salvatore Di Salvo <119612231+sdisalvo-crd@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:10:04 +0100 Subject: [PATCH 27/28] Revert "feat(ui): I already have a wallet (#521)" (#524) This reverts commit 48feccb0bd043dc71505d847086f32a97a1d457c. --- .../storage/secureStorage/secureStorage.ts | 3 - src/locales/en/en.json | 37 -- src/routes/backRoute/backRoute.test.ts | 4 +- src/routes/backRoute/backRoute.ts | 77 +-- src/routes/index.tsx | 7 - src/routes/nextRoute/nextRoute.test.ts | 2 - src/routes/nextRoute/nextRoute.ts | 52 +- src/routes/paths.ts | 1 - .../reducers/stateCache/stateCache.test.ts | 1 - src/store/reducers/stateCache/stateCache.ts | 3 +- .../reducers/stateCache/stateCache.types.ts | 1 - src/ui/App.tsx | 4 +- src/ui/components/AppWrapper/AppWrapper.tsx | 8 - .../SeedPhraseModule/SeedPhraseModule.scss | 21 - .../SeedPhraseModule/SeedPhraseModule.tsx | 192 +++---- .../SeedPhraseModule.types.ts | 11 +- .../pages/CreatePassword/CreatePassword.tsx | 2 - .../CreateSSIAgent/CreateSSIAgent.test.tsx | 150 +----- .../pages/CreateSSIAgent/CreateSSIAgent.tsx | 171 ++----- src/ui/pages/Onboarding/Onboarding.tsx | 13 +- .../VerifyRecoverySeedPhrase.scss | 93 ---- .../VerifyRecoverySeedPhrase.test.tsx | 368 ------------- .../VerifyRecoverySeedPhrase.tsx | 484 ------------------ .../VerifyRecoverySeedPhrase.types.ts | 6 - .../pages/VerifyRecoverySeedPhrase/index.ts | 1 - .../VerifySeedPhrase.test.tsx | 2 +- 26 files changed, 136 insertions(+), 1578 deletions(-) delete mode 100644 src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.scss delete mode 100644 src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx delete mode 100644 src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.tsx delete mode 100644 src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.types.ts delete mode 100644 src/ui/pages/VerifyRecoverySeedPhrase/index.ts diff --git a/src/core/storage/secureStorage/secureStorage.ts b/src/core/storage/secureStorage/secureStorage.ts index 09ac8bfd7..f18d90c89 100644 --- a/src/core/storage/secureStorage/secureStorage.ts +++ b/src/core/storage/secureStorage/secureStorage.ts @@ -6,11 +6,8 @@ import { enum KeyStoreKeys { APP_PASSCODE = "app-login-passcode", APP_OP_PASSWORD = "app-operations-password", - PASSWORD_SKIPPED = "app-password-skip", SIGNIFY_BRAN = "signify-bran", MEERKAT_SEED = "app-meerkat-seed", - RECOVERY_WALLET = "recovery-wallet", - RECOVERY_WALLET_LAST_FAIL_ATTEMPT_TIME = "recovery-wallet-last-fail-attempt-time", } class SecureStorage { diff --git a/src/locales/en/en.json b/src/locales/en/en.json index 54f662f62..9296d6bcc 100644 --- a/src/locales/en/en.json +++ b/src/locales/en/en.json @@ -588,42 +588,6 @@ } } }, - "verifyrecoveryseedphrase": { - "title": "Recover wallet", - "button": { - "continue": "Verify", - "lock": "Try again in 1 minute", - "clear": "Clear all" - }, - "paragraph": { - "top": "Please verify your seed phrase to recover your wallet. To start typing click on the first option." - }, - "suggestions": { - "title": "Suggestions", - "error": "All words must be compatible with the suggestions" - }, - "alert": { - "fail": { - "text": "Sorry, the seed phrase you have entered is incorrect!", - "button": { - "confirm": "Try again" - } - }, - "clear": { - "text": "Are you sure you want to clear all the words you have entered so far?", - "button": { - "confirm": "Clear all", - "cancel": "Cancel" - } - }, - "toomanyattempts": { - "text": "Too many failed attempts. Please try again later.", - "button": { - "confirm": "Ok" - } - } - } - }, "tabsmenu": { "label": { "identifiers": "Identity", @@ -679,7 +643,6 @@ "ssiagent": { "title": "Enter your SSI agent details", "description": "To continue, please enter the SSI agent boot and connect URLs (in your email or from your command line).", - "verifydescription": "To continue, please enter the SSI agent connect URLs (in your email or from your command line).", "button": { "info": "Get more information", "validate": "Validate" diff --git a/src/routes/backRoute/backRoute.test.ts b/src/routes/backRoute/backRoute.test.ts index 52d4cf8dc..5e9ee7da9 100644 --- a/src/routes/backRoute/backRoute.test.ts +++ b/src/routes/backRoute/backRoute.test.ts @@ -38,7 +38,6 @@ describe("getBackRoute", () => { userName: "", time: 0, ssiAgentIsSet: false, - recoveryWalletProgress: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { @@ -99,7 +98,7 @@ describe("getBackRoute", () => { const result = getBackRoute(currentPath, data); expect(result.backPath).toEqual({ pathname: "/route2" }); - expect(result.updateRedux).toHaveLength(4); + expect(result.updateRedux).toHaveLength(3); }); test("should return the correct back path when currentPath is /verifyseedphrase", () => { @@ -166,7 +165,6 @@ describe("getPreviousRoute", () => { userName: "", time: 0, ssiAgentIsSet: false, - recoveryWalletProgress: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { diff --git a/src/routes/backRoute/backRoute.ts b/src/routes/backRoute/backRoute.ts index 9e5303860..f1f870cfe 100644 --- a/src/routes/backRoute/backRoute.ts +++ b/src/routes/backRoute/backRoute.ts @@ -1,5 +1,4 @@ import { AnyAction, ThunkAction } from "@reduxjs/toolkit"; -import { SecureStorage } from "@aparajita/capacitor-secure-storage"; import { RootState } from "../../store"; import { removeCurrentRoute, @@ -9,56 +8,6 @@ import { import { clearSeedPhraseCache } from "../../store/reducers/seedPhraseCache"; import { DataProps, PayloadProps } from "../nextRoute/nextRoute.types"; import { RoutePath, TabsRoutePath } from "../paths"; -import { KeyStoreKeys } from "../../core/storage"; - -const getDefaultPreviousPath = (path: string, data: DataProps) => { - const isRecoveryMode = - data.store.stateCache.authentication.recoveryWalletProgress; - - if (RoutePath.SSI_AGENT === path) { - return isRecoveryMode - ? RoutePath.VERIFY_RECOVERY_SEED_PHRASE - : RoutePath.GENERATE_SEED_PHRASE; - } - - if (RoutePath.VERIFY_SEED_PHRASE === path) { - return RoutePath.GENERATE_SEED_PHRASE; - } - - if ( - [ - RoutePath.VERIFY_RECOVERY_SEED_PHRASE, - RoutePath.GENERATE_SEED_PHRASE, - ].includes(path as RoutePath) - ) { - return RoutePath.CREATE_PASSWORD; - } - - return RoutePath.ONBOARDING; -}; - -const clearSecureStore = (path: string) => { - if ( - [ - RoutePath.VERIFY_RECOVERY_SEED_PHRASE, - RoutePath.GENERATE_SEED_PHRASE, - ].includes(path as RoutePath) - ) { - SecureStorage.remove(KeyStoreKeys.PASSWORD_SKIPPED); - SecureStorage.remove(KeyStoreKeys.APP_OP_PASSWORD); - return; - } - - if (path === RoutePath.CREATE_PASSWORD) { - SecureStorage.remove(KeyStoreKeys.RECOVERY_WALLET); - return; - } - - if (path === RoutePath.SSI_AGENT) { - SecureStorage.remove(KeyStoreKeys.SIGNIFY_BRAN); - return; - } -}; const getBackRoute = ( currentPath: string, @@ -68,7 +17,6 @@ const getBackRoute = ( updateRedux: (() => ThunkAction)[]; } => { const { updateRedux } = backRoute[currentPath]; - clearSecureStore(currentPath); return { backPath: backPath(data), @@ -83,12 +31,11 @@ const updateStoreSetCurrentRoute = (data: DataProps) => { if (prevPath) { path = prevPath.path; } else { - path = getDefaultPreviousPath(data.store.stateCache.routes[0].path, data); + path = data.store.stateCache.routes[0].path; } return setCurrentRoute({ path }); }; - const getPreviousRoute = (data: DataProps): { pathname: string } => { const routes = data.store.stateCache.routes; @@ -100,7 +47,7 @@ const getPreviousRoute = (data: DataProps): { pathname: string } => { } else if (prevPath) { path = prevPath.path; } else { - path = getDefaultPreviousPath(routes[0].path, data); + path = routes[0].path; } return { pathname: path }; @@ -115,14 +62,6 @@ const calcPreviousRoute = ( const backPath = (data: DataProps) => getPreviousRoute(data); -const clearPasswordState = (data: DataProps) => { - return setAuthentication({ - ...data.store.stateCache.authentication, - passwordIsSkipped: false, - passwordIsSet: false, - }); -}; - const backRoute: Record = { [RoutePath.ROOT]: { updateRedux: [], @@ -135,27 +74,19 @@ const backRoute: Record = { removeCurrentRoute, updateStoreSetCurrentRoute, clearSeedPhraseCache, - clearPasswordState, ], }, [RoutePath.VERIFY_SEED_PHRASE]: { updateRedux: [removeCurrentRoute, updateStoreSetCurrentRoute], }, - [RoutePath.VERIFY_RECOVERY_SEED_PHRASE]: { - updateRedux: [ - removeCurrentRoute, - updateStoreSetCurrentRoute, - clearPasswordState, - ], - }, [RoutePath.SSI_AGENT]: { - updateRedux: [removeCurrentRoute, updateStoreSetCurrentRoute], + updateRedux: [], }, [RoutePath.SET_PASSCODE]: { updateRedux: [removeCurrentRoute, updateStoreSetCurrentRoute], }, [RoutePath.CREATE_PASSWORD]: { - updateRedux: [removeCurrentRoute, updateStoreSetCurrentRoute], + updateRedux: [], }, [RoutePath.CONNECTION_DETAILS]: { updateRedux: [removeCurrentRoute], diff --git a/src/routes/index.tsx b/src/routes/index.tsx index a5e74f71d..f32771248 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -20,7 +20,6 @@ import { IdentifierDetails } from "../ui/pages/IdentifierDetails"; import { CredentialDetails } from "../ui/pages/CredentialDetails"; import { ConnectionDetails } from "../ui/pages/ConnectionDetails"; import { CreateSSIAgent } from "../ui/pages/CreateSSIAgent"; -import { VerifyRecoverySeedPhrase } from "../ui/pages/VerifyRecoverySeedPhrase"; const Routes = () => { const stateCache = useAppSelector(getStateCache); @@ -74,12 +73,6 @@ const Routes = () => { exact /> - - { passwordIsSet: false, passwordIsSkipped: true, ssiAgentIsSet: false, - recoveryWalletProgress: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { @@ -167,7 +166,6 @@ describe("getNextRoute", () => { passwordIsSet: false, passwordIsSkipped: true, ssiAgentIsSet: false, - recoveryWalletProgress: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { diff --git a/src/routes/nextRoute/nextRoute.ts b/src/routes/nextRoute/nextRoute.ts index 0b85720b4..7bc153d0a 100644 --- a/src/routes/nextRoute/nextRoute.ts +++ b/src/routes/nextRoute/nextRoute.ts @@ -16,24 +16,15 @@ import { ToastMsgType } from "../../ui/globals/types"; const getNextRootRoute = (store: StoreState) => { const authentication = store.stateCache.authentication; - let path = RoutePath.ONBOARDING; - - if (authentication.passcodeIsSet) { - path = RoutePath.CREATE_PASSWORD; - } - - if (authentication.passwordIsSet || authentication.passwordIsSkipped) { - path = authentication.recoveryWalletProgress - ? RoutePath.VERIFY_RECOVERY_SEED_PHRASE - : RoutePath.GENERATE_SEED_PHRASE; - } - - if (authentication.seedPhraseIsSet) { - path = RoutePath.SSI_AGENT; - } - - if (authentication.ssiAgentIsSet) { + let path; + if ( + authentication.passcodeIsSet && + authentication.seedPhraseIsSet && + authentication.ssiAgentIsSet + ) { path = RoutePath.TABS_MENU; + } else { + path = RoutePath.ONBOARDING; } return { pathname: path }; @@ -47,9 +38,7 @@ const getNextOnboardingRoute = (data: DataProps) => { } if (data.store.stateCache.authentication.passwordIsSet) { - path = data.state?.recoveryWalletProgress - ? RoutePath.VERIFY_RECOVERY_SEED_PHRASE - : RoutePath.GENERATE_SEED_PHRASE; + path = RoutePath.GENERATE_SEED_PHRASE; } if (data.store.stateCache.authentication.seedPhraseIsSet) { @@ -115,15 +104,6 @@ const updateStoreAfterSetupSSI = (data: DataProps) => { return setAuthentication({ ...data.store.stateCache.authentication, ssiAgentIsSet: true, - recoveryWalletProgress: false, - seedPhraseIsSet: true, - }); -}; - -const updateStoreRecoveryWallet = (data: DataProps) => { - return setAuthentication({ - ...data.store.stateCache.authentication, - recoveryWalletProgress: data.state?.recoveryWalletProgress, }); }; @@ -151,11 +131,7 @@ const updateStoreCurrentRoute = (data: DataProps) => { return setCurrentRoute({ path: data.state?.nextRoute }); }; -const getNextCreatePasswordRoute = (data: DataProps) => { - if (data.store.stateCache.authentication.recoveryWalletProgress) { - return { pathname: RoutePath.VERIFY_RECOVERY_SEED_PHRASE }; - } - +const getNextCreatePasswordRoute = () => { return { pathname: RoutePath.GENERATE_SEED_PHRASE }; }; const updateStoreAfterCreatePassword = (data: DataProps) => { @@ -204,7 +180,7 @@ const nextRoute: Record = { }, [RoutePath.ONBOARDING]: { nextPath: (data: DataProps) => getNextOnboardingRoute(data), - updateRedux: [updateStoreRecoveryWallet], + updateRedux: [], }, [RoutePath.SET_PASSCODE]: { nextPath: (data: DataProps) => getNextSetPasscodeRoute(data.store), @@ -218,16 +194,12 @@ const nextRoute: Record = { nextPath: () => getNextVerifySeedPhraseRoute(), updateRedux: [updateStoreAfterVerifySeedPhraseRoute, clearSeedPhraseCache], }, - [RoutePath.VERIFY_RECOVERY_SEED_PHRASE]: { - nextPath: () => getNextVerifySeedPhraseRoute(), - updateRedux: [], - }, [RoutePath.SSI_AGENT]: { nextPath: () => getNextCreateSSIAgentRoute(), updateRedux: [updateStoreAfterSetupSSI], }, [RoutePath.CREATE_PASSWORD]: { - nextPath: (data: DataProps) => getNextCreatePasswordRoute(data), + nextPath: () => getNextCreatePasswordRoute(), updateRedux: [updateStoreAfterCreatePassword], }, [RoutePath.CONNECTION_DETAILS]: { diff --git a/src/routes/paths.ts b/src/routes/paths.ts index 496c9e5d5..02626ef40 100644 --- a/src/routes/paths.ts +++ b/src/routes/paths.ts @@ -8,7 +8,6 @@ enum RoutePath { CREATE_PASSWORD = "/createpassword", SSI_AGENT = "/ssiagent", CONNECTION_DETAILS = "/connectiondetails", - VERIFY_RECOVERY_SEED_PHRASE = "/verifyrecoveryseedphrase", } enum TabsRoutePath { diff --git a/src/store/reducers/stateCache/stateCache.test.ts b/src/store/reducers/stateCache/stateCache.test.ts index e15986ccd..8238648e1 100644 --- a/src/store/reducers/stateCache/stateCache.test.ts +++ b/src/store/reducers/stateCache/stateCache.test.ts @@ -57,7 +57,6 @@ describe("State Cache", () => { passwordIsSet: false, passwordIsSkipped: false, ssiAgentIsSet: false, - recoveryWalletProgress: false, }; const action = setAuthentication(authentication); const nextState = stateCacheSlice.reducer(initialState, action); diff --git a/src/store/reducers/stateCache/stateCache.ts b/src/store/reducers/stateCache/stateCache.ts index 201feca34..160d14b19 100644 --- a/src/store/reducers/stateCache/stateCache.ts +++ b/src/store/reducers/stateCache/stateCache.ts @@ -19,9 +19,8 @@ const initialState: StateCacheProps = { passcodeIsSet: false, seedPhraseIsSet: false, passwordIsSet: false, - passwordIsSkipped: false, + passwordIsSkipped: true, ssiAgentIsSet: false, - recoveryWalletProgress: false, }, currentOperation: OperationType.IDLE, queueIncomingRequest: { diff --git a/src/store/reducers/stateCache/stateCache.types.ts b/src/store/reducers/stateCache/stateCache.types.ts index ddd27f32e..7a15a075b 100644 --- a/src/store/reducers/stateCache/stateCache.types.ts +++ b/src/store/reducers/stateCache/stateCache.types.ts @@ -21,7 +21,6 @@ interface AuthenticationCacheProps { passwordIsSet: boolean; passwordIsSkipped: boolean; ssiAgentIsSet: boolean; - recoveryWalletProgress: boolean; } enum IncomingRequestType { CREDENTIAL_OFFER_RECEIVED = "credential-offer-received", diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 29b8260e3..16c583a6e 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -127,7 +127,9 @@ const App = () => { ); }; - const isPublicPage = PublicRoutes.includes(currentRoute?.path as RoutePath); + const isPublicPage = PublicRoutes.includes( + window.location.pathname as RoutePath + ); return ( diff --git a/src/ui/components/AppWrapper/AppWrapper.tsx b/src/ui/components/AppWrapper/AppWrapper.tsx index dc9b27c8c..7a76555f8 100644 --- a/src/ui/components/AppWrapper/AppWrapper.tsx +++ b/src/ui/components/AppWrapper/AppWrapper.tsx @@ -315,12 +315,6 @@ const AppWrapper = (props: { children: ReactNode }) => { let userName: { userName: string } = { userName: "" }; const passcodeIsSet = await checkKeyStore(KeyStoreKeys.APP_PASSCODE); const seedPhraseIsSet = await checkKeyStore(KeyStoreKeys.SIGNIFY_BRAN); - const recoveryWalletProgress = await checkKeyStore( - KeyStoreKeys.RECOVERY_WALLET - ); - const passwordIsSkipped = await checkKeyStore( - KeyStoreKeys.PASSWORD_SKIPPED - ); const passwordIsSet = await checkKeyStore(KeyStoreKeys.APP_OP_PASSWORD); const keriaConnectUrlRecord = await Agent.agent.basicStorage.findById( @@ -374,10 +368,8 @@ const AppWrapper = (props: { children: ReactNode }) => { passcodeIsSet, seedPhraseIsSet, passwordIsSet, - passwordIsSkipped, ssiAgentIsSet: !!keriaConnectUrlRecord && !!keriaConnectUrlRecord.content.url, - recoveryWalletProgress, }) ); diff --git a/src/ui/components/SeedPhraseModule/SeedPhraseModule.scss b/src/ui/components/SeedPhraseModule/SeedPhraseModule.scss index 50a40bef1..13370ff6e 100644 --- a/src/ui/components/SeedPhraseModule/SeedPhraseModule.scss +++ b/src/ui/components/SeedPhraseModule/SeedPhraseModule.scss @@ -50,26 +50,9 @@ padding-inline: 0.625rem; font-size: 1rem; - &:focus-within { - border: 1px dashed var(--ion-color-secondary); - background-color: transparent; - } - - .word-input { - min-height: auto; - - .input-highlight.sc-ion-input-md { - display: none; - } - } - &.empty-word { border: 1px dashed var(--ion-color-dark-grey); background-color: transparent; - - &.error { - border-color: var(--ion-color-danger); - } } .index { @@ -89,10 +72,6 @@ font-size: 0.8rem; } - .word-input { - font-size: 0.8rem; - } - .index { margin-right: 0.125rem; } diff --git a/src/ui/components/SeedPhraseModule/SeedPhraseModule.tsx b/src/ui/components/SeedPhraseModule/SeedPhraseModule.tsx index f10ec3d15..f7f4775f1 100644 --- a/src/ui/components/SeedPhraseModule/SeedPhraseModule.tsx +++ b/src/ui/components/SeedPhraseModule/SeedPhraseModule.tsx @@ -1,136 +1,82 @@ -import { IonButton, IonChip, IonIcon, IonInput } from "@ionic/react"; +import { IonButton, IonChip, IonIcon } from "@ionic/react"; import { eyeOffOutline } from "ionicons/icons"; import { i18n } from "../../../i18n"; import "./SeedPhraseModule.scss"; -import { - SeedPhraseModuleProps, - SeedPhraseModuleRef, -} from "./SeedPhraseModule.types"; -import { forwardRef, useImperativeHandle, useRef } from "react"; -import { combineClassNames } from "../../utils/style"; +import { SeedPhraseModuleProps } from "./SeedPhraseModule.types"; -const SeedPhraseModule = forwardRef( - ( - { - testId, - seedPhrase, - hideSeedPhrase, - setHideSeedPhrase, - addSeedPhraseSelected, - removeSeedPhraseSelected, - emptyWord, - hideSeedNumber, - inputMode, - errorInputIndexs, - onInputChange, - onInputBlur, - onInputFocus, - }, - ref - ) => { - const seedInputs = useRef<(HTMLElement | null)[]>([]); - - useImperativeHandle(ref, () => ({ - focusInputByIndex: (index) => { - const input = seedInputs.current.at(index); - if (!input) return; - - (input as any).setFocus(); - }, - })); - - const getClassName = (word: string, index: number) => { - if (!inputMode) return; - - return combineClassNames("seed-chips", { - "empty-word": !word && index === seedPhrase.length - 1, - "empty-word error": - !!errorInputIndexs?.includes(index) || - (!word && index !== seedPhrase.length - 1), - }); - }; - - return ( +const SeedPhraseModule = ({ + testId, + seedPhrase, + hideSeedPhrase, + setHideSeedPhrase, + addSeedPhraseSelected, + removeSeedPhraseSelected, + emptyWord, + hideSeedNumber, +}: SeedPhraseModuleProps) => { + return ( +
-
+

+ {i18n.t("generateseedphrase.privacy.overlay.text")} +

+ setHideSeedPhrase && setHideSeedPhrase(false)} > - -

- {i18n.t("generateseedphrase.privacy.overlay.text")} -

- setHideSeedPhrase && setHideSeedPhrase(false)} - > - {i18n.t("generateseedphrase.privacy.overlay.button")} - -
-
- {seedPhrase.map((word, index) => { - return ( - { - if (removeSeedPhraseSelected) { - removeSeedPhraseSelected(index); - } else if (addSeedPhraseSelected) { - addSeedPhraseSelected(word); - } - }} - className={getClassName(word, index)} - > - {!hideSeedNumber && ( - - {index + 1}. - - )} - {inputMode ? ( - (seedInputs.current[index] = ref)} - value={word} - onIonInput={(e) => { - onInputChange?.(e.target.value as string, index); - }} - onIonFocus={() => onInputFocus?.(index)} - onIonBlur={() => onInputBlur?.(index)} - name={`word-input-${index}`} - id={`word-input-${index}`} - data-testid={`word-input-${index}`} - /> - ) : ( - {word} - )} - - ); - })} - {emptyWord && ( + {i18n.t("generateseedphrase.privacy.overlay.button")} + +
+
+ {seedPhrase.map((word, index) => { + return ( { + if (removeSeedPhraseSelected) { + removeSeedPhraseSelected(index); + } else if (addSeedPhraseSelected) { + addSeedPhraseSelected(word); + } + }} > - {seedPhrase.length + 1}. + {!hideSeedNumber && ( + + {index + 1}. + + )} + {word} - )} -
+ ); + })} + {emptyWord && ( + + {seedPhrase.length + 1}. + + )}
- ); - } -); +
+ ); +}; export { SeedPhraseModule }; diff --git a/src/ui/components/SeedPhraseModule/SeedPhraseModule.types.ts b/src/ui/components/SeedPhraseModule/SeedPhraseModule.types.ts index 8280e82d4..d856d3184 100644 --- a/src/ui/components/SeedPhraseModule/SeedPhraseModule.types.ts +++ b/src/ui/components/SeedPhraseModule/SeedPhraseModule.types.ts @@ -7,15 +7,6 @@ interface SeedPhraseModuleProps { removeSeedPhraseSelected?: (index: number) => void; emptyWord?: boolean; hideSeedNumber?: boolean; - inputMode?: boolean; - onInputChange?: (value: string, index: number) => void; - onInputFocus?: (index: number) => void; - onInputBlur?: (index: number) => void; - errorInputIndexs?: number[]; } -interface SeedPhraseModuleRef { - focusInputByIndex: (index: number) => void; -} - -export type { SeedPhraseModuleProps, SeedPhraseModuleRef }; +export type { SeedPhraseModuleProps }; diff --git a/src/ui/pages/CreatePassword/CreatePassword.tsx b/src/ui/pages/CreatePassword/CreatePassword.tsx index 8cea93518..f94ac1582 100644 --- a/src/ui/pages/CreatePassword/CreatePassword.tsx +++ b/src/ui/pages/CreatePassword/CreatePassword.tsx @@ -72,8 +72,6 @@ const CreatePassword = () => { }) ); } - } else { - await SecureStorage.set(KeyStoreKeys.PASSWORD_SKIPPED, String(true)); } const { nextPath, updateRedux } = getNextRoute(RoutePath.CREATE_PASSWORD, { diff --git a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx index 497db0819..d44d548a1 100644 --- a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx +++ b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.test.tsx @@ -13,17 +13,15 @@ import { setCurrentOperation } from "../../../store/reducers/stateCache"; import { OperationType } from "../../globals/types"; import { setBootUrl, setConnectUrl } from "../../../store/reducers/ssiAgent"; import { RoutePath } from "../../../routes"; -import { KeyStoreKeys } from "../../../core/storage"; +import { Agent } from "../../../core/agent/agent"; const bootAndConnectMock = jest.fn((...args: any) => Promise.resolve()); -const recoverKeriaAgentMock = jest.fn(); jest.mock("../../../core/agent/agent", () => ({ Agent: { ...jest.requireActual("../../../core/agent/agent"), agent: { bootAndConnect: (...args: any) => bootAndConnectMock(...args), - recoverKeriaAgent: (...args: any) => recoverKeriaAgentMock(...args), }, }, })); @@ -69,15 +67,6 @@ jest.mock("../../components/CustomInput", () => ({ }, })); -const secureStorageDeleteFunc = jest.fn(); - -jest.mock("../../../core/storage", () => ({ - ...jest.requireActual("../../../core/storage"), - SecureStorage: { - delete: (...args: any) => secureStorageDeleteFunc(...args), - }, -})); - describe("SSI agent page", () => { const mockStore = configureStore(); const dispatchMock = jest.fn(); @@ -86,14 +75,6 @@ describe("SSI agent page", () => { bootUrl: undefined, connectUrl: undefined, }, - stateCache: { - authentication: { - loggedIn: true, - time: Date.now(), - passcodeIsSet: true, - recoveryWalletProgress: false, - }, - }, }; const storeMocked = { @@ -175,14 +156,6 @@ describe("SSI agent page", () => { bootUrl: "11111", connectUrl: undefined, }, - stateCache: { - authentication: { - loggedIn: true, - time: Date.now(), - passcodeIsSet: true, - recoveryWalletProgress: false, - }, - }, }; const storeMocked = { @@ -212,14 +185,6 @@ describe("SSI agent page", () => { bootUrl: undefined, connectUrl: "11111", }, - stateCache: { - authentication: { - loggedIn: true, - time: Date.now(), - passcodeIsSet: true, - recoveryWalletProgress: false, - }, - }, }; const storeMocked = { @@ -251,14 +216,6 @@ describe("SSI agent page", () => { bootUrl: undefined, connectUrl: "https://connectUrl.com/", }, - stateCache: { - authentication: { - loggedIn: true, - time: Date.now(), - passcodeIsSet: true, - recoveryWalletProgress: false, - }, - }, }; const storeMocked = { @@ -290,14 +247,6 @@ describe("SSI agent page", () => { bootUrl: "11111", connectUrl: "11111", }, - stateCache: { - authentication: { - loggedIn: true, - time: Date.now(), - passcodeIsSet: true, - recoveryWalletProgress: false, - }, - }, }; const storeMocked = { @@ -371,100 +320,3 @@ describe("SSI agent page", () => { }); }); }); - -describe("SSI agent page: recovery mode", () => { - const mockStore = configureStore(); - const dispatchMock = jest.fn(); - const initialState = { - ssiAgentCache: { - bootUrl: undefined, - connectUrl: undefined, - }, - stateCache: { - authentication: { - loggedIn: true, - time: Date.now(), - passcodeIsSet: true, - recoveryWalletProgress: true, - }, - }, - }; - - const storeMocked = { - ...mockStore(initialState), - dispatch: dispatchMock, - }; - - test("Renders ssi agent page", () => { - const { getByText, getByTestId, queryByTestId } = render( - - - - ); - - expect(getByText(ENG_Trans.ssiagent.title)).toBeVisible(); - expect(getByText(ENG_Trans.ssiagent.verifydescription)).toBeVisible(); - expect(getByText(ENG_Trans.ssiagent.button.info)).toBeVisible(); - expect(getByText(ENG_Trans.ssiagent.button.validate)).toBeVisible(); - expect( - getByText(ENG_Trans.ssiagent.button.validate).getAttribute("disabled") - ).toBe("true"); - - expect(queryByTestId("boot-url-input")).toBe(null); - expect(getByTestId("connect-url-input")).toBeVisible(); - }); - - test("Connect and boot success", async () => { - const mockStore = configureStore(); - const initialState = { - stateCache: { - authentication: { - passcodeIsSet: true, - seedPhraseIsSet: true, - passwordIsSet: true, - passwordIsSkipped: true, - loggedIn: false, - userName: "", - time: 0, - ssiAgentIsSet: false, - recoveryWalletProgress: true, - }, - }, - ssiAgentCache: { - bootUrl: - "https://dev.keria-boot.cf-keripy.metadata.dev.cf-deployments.org", - connectUrl: - "https://dev.keria.cf-keripy.metadata.dev.cf-deployments.org", - }, - seedPhraseCache: { - seedPhrase: "mock-seed", - }, - }; - - const storeMocked = { - ...mockStore(initialState), - dispatch: dispatchMock, - }; - - const history = createMemoryHistory(); - history.push(RoutePath.SSI_AGENT); - - const { getByTestId } = render( - - - - - - ); - - act(() => { - fireEvent.click(getByTestId("primary-button-create-ssi-agent")); - }); - - await waitFor(() => { - expect(secureStorageDeleteFunc).toBeCalledWith( - KeyStoreKeys.RECOVERY_WALLET - ); - }); - }); -}); diff --git a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx index 52aac2291..efd4f8351 100644 --- a/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx +++ b/src/ui/pages/CreateSSIAgent/CreateSSIAgent.tsx @@ -33,11 +33,8 @@ import { TermsModal } from "../../components/TermsModal"; import { Agent } from "../../../core/agent/agent"; import { ScrollablePageLayout } from "../../components/layout/ScrollablePageLayout"; import { ConfigurationService } from "../../../core/configuration"; -import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; -import { getSeedPhraseCache } from "../../../store/reducers/seedPhraseCache"; const SSI_URLS_EMPTY = "SSI url is empty"; -const SEED_PHRASE_EMPTY = "Invalid seed phrase"; const InputError = ({ showError, @@ -56,9 +53,8 @@ const InputError = ({ const CreateSSIAgent = () => { const pageId = "create-ssi-agent"; const ssiAgent = useAppSelector(getSSIAgent); - const seedPhraseCache = useAppSelector(getSeedPhraseCache); - const stateCache = useAppSelector(getStateCache); + const stateCache = useAppSelector(getStateCache); const ionRouter = useAppIonRouter(); const dispatch = useAppDispatch(); const [connectUrlInputTouched, setConnectUrlTouched] = useState(false); @@ -67,9 +63,6 @@ const CreateSSIAgent = () => { const [loading, setLoading] = useState(false); const [hasMismatchError, setHasMismatchError] = useState(false); const [isInvalidBootUrl, setIsInvalidBootUrl] = useState(false); - const [isInvalidConnectUrl, setInvalidConnectUrl] = useState(false); - - const isRecoveryMode = stateCache.authentication.recoveryWalletProgress; useEffect(() => { if (!ssiAgent.bootUrl && !ssiAgent.connectUrl) { @@ -91,9 +84,7 @@ const CreateSSIAgent = () => { }; const validBootUrl = useMemo(() => { - return ( - isRecoveryMode || (ssiAgent.bootUrl && isValidHttpUrl(ssiAgent.bootUrl)) - ); + return ssiAgent.bootUrl && isValidHttpUrl(ssiAgent.bootUrl); }, [ssiAgent]); const validConnectUrl = useMemo(() => { @@ -101,7 +92,6 @@ const CreateSSIAgent = () => { }, [ssiAgent]); const displayBootUrlError = - !isRecoveryMode && bootUrlInputTouched && ssiAgent.bootUrl && !isValidHttpUrl(ssiAgent.bootUrl); @@ -117,62 +107,7 @@ const CreateSSIAgent = () => { dispatch(clearSSIAgent()); }; - const handleRecoveryWallet = async () => { - setLoading(true); - try { - if (!ssiAgent.connectUrl) { - throw new Error(SSI_URLS_EMPTY); - } - - if (!seedPhraseCache.seedPhrase) { - throw new Error(SEED_PHRASE_EMPTY); - } - - await Agent.agent.recoverKeriaAgent( - seedPhraseCache.seedPhrase.split(" "), - ssiAgent.connectUrl - ); - - const { nextPath, updateRedux } = getNextRoute(RoutePath.SSI_AGENT, { - store: { stateCache }, - }); - - updateReduxState( - nextPath.pathname, - { - store: { stateCache }, - }, - dispatch, - updateRedux - ); - - SecureStorage.delete(KeyStoreKeys.RECOVERY_WALLET); - - ionRouter.push(nextPath.pathname, "forward", "push"); - handleClearState(); - } catch (e) { - const errorMessage = (e as Error).message; - - if ( - [SSI_URLS_EMPTY, SEED_PHRASE_EMPTY, Agent.INVALID_MNEMONIC].includes( - errorMessage - ) - ) { - return; - } - - if (Agent.KERIA_NOT_BOOTED === errorMessage) { - setHasMismatchError(true); - return; - } - - setInvalidConnectUrl(true); - } finally { - setLoading(false); - } - }; - - const handleCreateSSI = async () => { + const handleValidate = async () => { setLoading(true); try { if (!ssiAgent.bootUrl || !ssiAgent.connectUrl) { @@ -180,7 +115,7 @@ const CreateSSIAgent = () => { } await Agent.agent.bootAndConnect({ - bootUrl: ssiAgent.bootUrl || "", + bootUrl: ssiAgent.bootUrl, url: ssiAgent.connectUrl, }); @@ -213,14 +148,6 @@ const CreateSSIAgent = () => { } }; - const handleValidate = () => { - if (isRecoveryMode) { - handleRecoveryWallet(); - } else { - handleCreateSSI(); - } - }; - const scanBootUrl = (event: ReactMouseEvent) => { event.stopPropagation(); dispatch(setCurrentOperation(OperationType.SCAN_SSI_BOOT_URL)); @@ -241,17 +168,6 @@ const CreateSSIAgent = () => { return result; }; - const handleChangeConnectUrl = (connectionUrl: string) => { - setInvalidConnectUrl(false); - setHasMismatchError(false); - dispatch(setConnectUrl(connectionUrl)); - }; - - const handleChangeBootUrl = (bootUrl: string) => { - setIsInvalidBootUrl(false); - dispatch(setBootUrl(bootUrl)); - }; - return ( <> { className="page-paragraph" data-testid={`${pageId}-top-paragraph`} > - {i18n.t( - isRecoveryMode - ? "ssiagent.verifydescription" - : "ssiagent.description" - )} + {i18n.t("ssiagent.description")}

{ {i18n.t("ssiagent.button.info")}
- {!isRecoveryMode && ( - <> - { - setTouchedBootUrlInput(); + { + setIsInvalidBootUrl(false); + dispatch(setBootUrl(bootUrl)); + }} + value={ssiAgent.bootUrl || ""} + onChangeFocus={(result) => { + setTouchedBootUrlInput(); - if (!result && ssiAgent.bootUrl) { - dispatch( - setBootUrl(removeLastSlash(ssiAgent.bootUrl.trim())) - ); - } - }} - error={!!displayBootUrlError || isInvalidBootUrl} - /> - - - )} + if (!result && ssiAgent.bootUrl) { + dispatch(setBootUrl(removeLastSlash(ssiAgent.bootUrl.trim()))); + } + }} + error={!!displayBootUrlError || isInvalidBootUrl} + /> + { + setHasMismatchError(false); + dispatch(setConnectUrl(connectionUrl)); + }} onChangeFocus={(result) => { setTouchedConnectUrlInput(); @@ -340,18 +251,14 @@ const CreateSSIAgent = () => { } }} value={ssiAgent.connectUrl || ""} - error={ - !!displayConnectUrlError || hasMismatchError || isInvalidConnectUrl - } + error={!!displayConnectUrlError || hasMismatchError} /> { // @TODO - foconnor: This should be op: OperationType when available (non optional) const handleNavigation = (op?: string) => { + if (op) { + // @TODO - sdisalvo: Remove this condition and default to dispatch when the restore route is ready + return; + } const data: DataProps = { store: { stateCache }, - state: { - recoveryWalletProgress: !!op, - }, }; const { nextPath, updateRedux } = getNextRoute(RoutePath.ONBOARDING, data); updateReduxState(nextPath.pathname, data, dispatch, updateRedux); - - if (op) { - SecureStorage.set(KeyStoreKeys.RECOVERY_WALLET, String(!!op)); - } - history.push({ pathname: nextPath.pathname, state: data.state, diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.scss b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.scss deleted file mode 100644 index 5b86ce413..000000000 --- a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.scss +++ /dev/null @@ -1,93 +0,0 @@ -.verify-recovery-seed-phrase { - --background: var(--ion-color-light); - - .seed-phrase-module:nth-of-type(1) { - background: var(--ion-color-light-grey); - } - - .page-header { - ion-toolbar { - --background: transparent; - } - } - - .page-title { - font-size: 1.5rem; - line-height: 1.75rem; - margin-top: 0.5rem; - font-weight: 700; - } - - .content-container { - display: flex; - flex-direction: column; - justify-content: space-between; - min-height: calc(100vh - (5.25rem + var(--ion-safe-area-top))); - - .page-content { - display: flex; - flex-direction: column; - - & > * { - flex: 0 1 auto; - } - } - } - - .paragraph-top { - margin: 0.75rem 0 1rem; - } - - .suggest-error { - font-size: 1rem; - line-height: 1.115rem; - color: var(--ion-color-primary); - text-align: center; - font-weight: 400; - } - - .suggestion-title { - margin: 1.5rem 0 0.75rem; - font-size: 1rem; - font-weight: 600; - line-height: 1.5rem; - } - - .seed-phrase-module:nth-of-type(2) { - border: none; - } - - .clear-button { - display: flex; - width: fit-content; - margin: 1.5rem auto 1rem; - } - - @media screen and (min-width: 250px) and (max-width: 370px) { - .responsive-page-layout { - padding: 0 0.9rem; - - .responsive-page-content { - & > * { - margin: 0; - } - - & > .paragraph-top { - margin: 0.2rem 0; - } - } - } - - .suggest-error { - font-size: 0.8rem; - } - - .clear-button { - margin-top: 1rem; - } - - .suggestion-title { - margin-top: 1rem; - } - } -} diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx deleted file mode 100644 index 78710611e..000000000 --- a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.test.tsx +++ /dev/null @@ -1,368 +0,0 @@ -import { IonReactMemoryRouter } from "@ionic/react-router"; -import { fireEvent, render, waitFor } from "@testing-library/react"; -import { createMemoryHistory } from "history"; -import { act } from "react-dom/test-utils"; -import { Provider } from "react-redux"; -import configureStore from "redux-mock-store"; -import ENG_Trans from "../../../locales/en/en.json"; -import { RoutePath } from "../../../routes"; -import { VerifyRecoverySeedPhrase } from "./VerifyRecoverySeedPhrase"; -import { setSeedPhraseCache } from "../../../store/reducers/seedPhraseCache"; - -const SEED_PHRASE_LENGTH = 18; - -const secureStorageGetFunc = jest.fn(); -const secureStorageSetFunc = jest.fn(); -const secureStorageDeleteFunc = jest.fn(); -const verifySeedPhraseFnc = jest.fn(); - -jest.mock("../../../core/agent/agent", () => ({ - Agent: { - agent: { - isMnemonicValid: () => verifySeedPhraseFnc(), - }, - }, -})); - -jest.mock("../../../core/storage", () => ({ - ...jest.requireActual("../../../core/storage"), - SecureStorage: { - get: (...args: any) => secureStorageGetFunc(...args), - set: (...args: any) => secureStorageSetFunc(...args), - delete: (...args: any) => secureStorageDeleteFunc(...args), - }, -})); - -jest.mock("@ionic/react", () => ({ - ...jest.requireActual("@ionic/react"), - IonInput: (props: any) => { - return ( - props.onIonBlur(e)} - onFocus={(e) => props.onIonFocus(e)} - onChange={(e) => props.onIonInput?.(e)} - /> - ); - }, -})); - -describe("Verify Recovery Seed Phrase", () => { - const mockStore = configureStore(); - const dispatchMock = jest.fn(); - const initialState = { - stateCache: { - authentication: { - loggedIn: true, - time: Date.now(), - passcodeIsSet: true, - recoveryWalletProgress: true, - }, - }, - }; - - const storeMocked = { - ...mockStore(initialState), - dispatch: dispatchMock, - }; - - test("Render screen", () => { - const { getByText, getByTestId } = render( - - - - ); - - expect(getByText(ENG_Trans.verifyrecoveryseedphrase.title)).toBeVisible(); - expect( - getByText(ENG_Trans.verifyrecoveryseedphrase.paragraph.top) - ).toBeVisible(); - expect( - getByText( - ENG_Trans.verifyrecoveryseedphrase.button.continue - ).getAttribute("disabled") - ).toBe("true"); - expect(getByTestId("word-input-0")).toBeVisible(); - }); - - test("Render suggest seed phrase", async () => { - const { getByText, getByTestId } = render( - - - - ); - - expect(getByText(ENG_Trans.verifyrecoveryseedphrase.title)).toBeVisible(); - - const firstInput = getByTestId("word-input-0"); - act(() => { - fireEvent.focus(firstInput); - fireEvent.change(firstInput, { - target: { value: "a" }, - }); - }); - - expect( - getByText(ENG_Trans.verifyrecoveryseedphrase.suggestions.title) - ).toBeVisible(); - expect(getByText("abandon")).toBeVisible(); - expect(getByTestId("word-input-1")).toBeVisible(); - - act(() => { - fireEvent.click(getByText("abandon")); - }); - - await waitFor(() => { - expect((firstInput as HTMLInputElement).value).toEqual("abandon"); - }); - }); - - test("Render/clear word should match suggestion error", async () => { - const { getByText, getByTestId, queryByTestId } = render( - - - - ); - - expect(getByText(ENG_Trans.verifyrecoveryseedphrase.title)).toBeVisible(); - - const firstInput = getByTestId("word-input-0"); - act(() => { - fireEvent.focus(firstInput); - fireEvent.change(firstInput, { - target: { value: "a" }, - }); - }); - - expect( - getByText(ENG_Trans.verifyrecoveryseedphrase.suggestions.title) - ).toBeVisible(); - - act(() => { - fireEvent.blur(firstInput); - }); - - await waitFor(() => { - expect(getByTestId("no-suggest-error")).toBeVisible(); - }); - - act(() => { - fireEvent.focus(firstInput); - fireEvent.change(firstInput, { - target: { value: "abandon" }, - }); - }); - - expect( - getByText(ENG_Trans.verifyrecoveryseedphrase.suggestions.title) - ).toBeVisible(); - - act(() => { - fireEvent.blur(firstInput); - }); - - await waitFor(() => { - expect(queryByTestId("no-suggest-error")).toBe(null); - }); - }); - - test("Fill all seed", async () => { - const history = createMemoryHistory(); - history.push(RoutePath.VERIFY_RECOVERY_SEED_PHRASE); - - verifySeedPhraseFnc.mockImplementation(() => { - return Promise.resolve(true); - }); - - const { getByText, getByTestId } = render( - - - - - - ); - - expect(getByText(ENG_Trans.verifyrecoveryseedphrase.title)).toBeVisible(); - for (let i = 0; i < SEED_PHRASE_LENGTH; i++) { - act(() => { - const input = getByTestId(`word-input-${i}`); - fireEvent.focus(input); - fireEvent.change(input, { - target: { value: "a" }, - }); - }); - - await waitFor(() => { - expect(getByText("abandon")).toBeVisible(); - }); - - act(() => { - fireEvent.click(getByText("abandon")); - }); - - if (i < SEED_PHRASE_LENGTH - 1) { - await waitFor(() => { - expect(getByTestId(`word-input-${i}`)).toBeVisible(); - }); - } - } - - expect( - getByText( - ENG_Trans.verifyrecoveryseedphrase.button.continue - ).getAttribute("disabled") - ).toBe("false"); - - act(() => { - fireEvent.click( - getByText(ENG_Trans.verifyrecoveryseedphrase.button.continue) - ); - }); - - await waitFor(() => { - expect(dispatchMock).toBeCalledWith( - setSeedPhraseCache({ - seedPhrase: - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon", - bran: "", - }) - ); - }); - }); - - test("Lock when try max attempt", async () => { - const history = createMemoryHistory(); - history.push(RoutePath.VERIFY_RECOVERY_SEED_PHRASE); - - const { getByText, getByTestId } = render( - - - - - - ); - - expect(getByText(ENG_Trans.verifyrecoveryseedphrase.title)).toBeVisible(); - for (let i = 0; i < SEED_PHRASE_LENGTH; i++) { - act(() => { - const input = getByTestId(`word-input-${i}`); - fireEvent.focus(input); - fireEvent.change(input, { - target: { value: "a" }, - }); - }); - - await waitFor(() => { - expect(getByText("abandon")).toBeVisible(); - }); - - act(() => { - fireEvent.click(getByText("abandon")); - }); - - if (i < SEED_PHRASE_LENGTH - 1) { - await waitFor(() => { - expect(getByTestId(`word-input-${i}`)).toBeVisible(); - }); - } - } - - expect( - getByText( - ENG_Trans.verifyrecoveryseedphrase.button.continue - ).getAttribute("disabled") - ).toBe("false"); - - verifySeedPhraseFnc.mockImplementation(() => { - return Promise.resolve(false); - }); - - for (let i = 0; i < 5; i++) { - act(() => { - fireEvent.click( - getByText(ENG_Trans.verifyrecoveryseedphrase.button.continue) - ); - }); - - await waitFor(() => { - expect( - getByText(ENG_Trans.verifyrecoveryseedphrase.alert.fail.text) - ).toBeVisible(); - }); - - act(() => { - fireEvent.click( - getByText( - ENG_Trans.verifyrecoveryseedphrase.alert.fail.button.confirm - ) - ); - }); - } - - await waitFor(() => { - expect( - getByText(ENG_Trans.verifyrecoveryseedphrase.alert.toomanyattempts.text) - ).toBeVisible(); - expect( - getByText(ENG_Trans.verifyrecoveryseedphrase.button.lock) - ).toBeVisible(); - }); - }); - - test("Lock when try max attempt after reload page", async () => { - const history = createMemoryHistory(); - history.push(RoutePath.VERIFY_RECOVERY_SEED_PHRASE); - - secureStorageGetFunc.mockImplementation(() => { - return Promise.resolve(Date.now() - 1000); - }); - - const { getByText } = render( - - - - - - ); - - await waitFor(() => { - expect( - getByText(ENG_Trans.verifyrecoveryseedphrase.button.lock) - ).toBeVisible(); - }); - }); - - test("Remove last lock time when duration greater than 10 minutes", async () => { - const history = createMemoryHistory(); - history.push(RoutePath.VERIFY_RECOVERY_SEED_PHRASE); - - secureStorageGetFunc.mockImplementation(() => { - return Promise.resolve(Date.now() - 11 * 60 * 1000); - }); - - render( - - - - - - ); - - await waitFor(() => { - expect(secureStorageDeleteFunc).toBeCalled(); - }); - }); -}); diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.tsx b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.tsx deleted file mode 100644 index 22319472f..000000000 --- a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.tsx +++ /dev/null @@ -1,484 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { useHistory } from "react-router-dom"; -import { closeOutline } from "ionicons/icons"; -import { wordlists } from "bip39"; -import { IonButton, IonIcon } from "@ionic/react"; -import { i18n } from "../../../i18n"; -import { RoutePath } from "../../../routes"; -import { useAppDispatch, useAppSelector } from "../../../store/hooks"; -import { Alert as AlertFail } from "../../components/Alert"; -import "./VerifyRecoverySeedPhrase.scss"; -import { getNextRoute } from "../../../routes/nextRoute"; -import { updateReduxState } from "../../../store/utils"; -import { getStateCache } from "../../../store/reducers/stateCache"; -import { getBackRoute } from "../../../routes/backRoute"; -import { DataProps } from "../../../routes/nextRoute/nextRoute.types"; -import { PageHeader } from "../../components/PageHeader"; -import { PageFooter } from "../../components/PageFooter"; -import { SeedPhraseModule } from "../../components/SeedPhraseModule"; -import { useAppIonRouter } from "../../hooks"; -import { ScrollablePageLayout } from "../../components/layout/ScrollablePageLayout"; -import { SeedPhraseModuleRef } from "../../components/SeedPhraseModule/SeedPhraseModule.types"; -import { KeyStoreKeys, SecureStorage } from "../../../core/storage"; -import { setSeedPhraseCache } from "../../../store/reducers/seedPhraseCache"; -import { Agent } from "../../../core/agent/agent"; -import { SeedPhraseInfo } from "./VerifyRecoverySeedPhrase.types"; - -const SEED_PHRASE_LENGTH = 18; -const SUGGEST_SEED_PHRASE_LENGTH = 4; -const MAX_ATTEMPT_FAIL = 5; -const LOCK_TIME = 60000; -const RESET_ATTEMPT_TIME = 600000; -const SELECT_WORD_LIST = wordlists.english; -const INVALID_SEED_PHRASE = "Invalid seed phrase"; - -const VerifyRecoverySeedPhrase = () => { - const pageId = "verify-recovery-seed-phrase"; - const history = useHistory(); - const dispatch = useAppDispatch(); - const stateCache = useAppSelector(getStateCache); - - const [alertIsOpen, setAlertIsOpen] = useState(false); - const [clearAlertOpen, setClearAlertOpen] = useState(false); - const [alertManyAttempOpen, setAlertManyAttempOpen] = useState(false); - const ionRouter = useAppIonRouter(); - - const [seedPhraseInfo, setSeedPhraseInfo] = useState([ - { - value: "", - suggestions: [], - }, - ]); - - const [failAttempt, setFailAttempt] = useState(0); - const [lastLockTime, setLastLockTime] = useState(null); - const [lockFailAttempt, setLockFailAttempt] = useState(false); - const [lastFocusIndex, setLastFocusIndex] = useState(null); - const [isTyping, setIsTyping] = useState(false); - - const seedPhraseRef = useRef(null); - - const seedPhrase = seedPhraseInfo.map((item) => item.value); - const suggestSeedPhrase = - (isTyping && - seedPhraseInfo.find((_, index) => index === lastFocusIndex) - ?.suggestions) || - []; - const errorInputIndex = seedPhraseInfo.reduce((result, nextItem, index) => { - if (nextItem.suggestions.includes(nextItem.value)) return result; - - if (nextItem.value && nextItem.suggestions.length === 0) { - result.push(index); - } - - if ( - nextItem.value && - nextItem.suggestions.length && - (index !== lastFocusIndex || !isTyping) - ) { - result.push(index); - } - - if (!nextItem.value && index !== seedPhrase.length - 1) { - result.push(index); - } - - return result; - }, [] as number[]); - - const removeLockAttempt = () => { - SecureStorage.delete(KeyStoreKeys.RECOVERY_WALLET_LAST_FAIL_ATTEMPT_TIME); - setFailAttempt(0); - setLockFailAttempt(false); - }; - - const getLockDuration = useCallback(() => { - return Date.now() - (!lastLockTime ? 0 : lastLockTime.getTime()); - }, [lastLockTime]); - - useEffect(() => { - async function getFailAttemptInfo() { - try { - const lastFailedTime = await SecureStorage.get( - KeyStoreKeys.RECOVERY_WALLET_LAST_FAIL_ATTEMPT_TIME - ); - if (!lastFailedTime) return; - - const lockDuration = Date.now() - Number(lastFailedTime); - - if (lockDuration >= RESET_ATTEMPT_TIME) { - removeLockAttempt(); - return; - } - - setFailAttempt(MAX_ATTEMPT_FAIL); - setLastLockTime(new Date(Number(lastFailedTime))); - } catch (e) { - // TODO: handle error - } - } - - getFailAttemptInfo(); - }, []); - - // Reset attempt numbers after 10 minutes from last lock period - useEffect(() => { - if (!lastLockTime) return; - - const resetAttemptTime = RESET_ATTEMPT_TIME - getLockDuration(); - - if (resetAttemptTime <= 0) return; - - const timerId = setTimeout(() => { - removeLockAttempt(); - }, resetAttemptTime); - - return () => { - clearTimeout(timerId); - }; - }, [lastLockTime]); - - useEffect(() => { - if (!lastLockTime) return; - - const lockTime = LOCK_TIME - getLockDuration(); - - if (lockTime <= 0) { - setLockFailAttempt(false); - return; - } - - setLockFailAttempt(true); - setFailAttempt(MAX_ATTEMPT_FAIL); - - const timerId = setTimeout(() => { - setLockFailAttempt(false); - }, lockTime); - - return () => { - clearTimeout(timerId); - }; - }, [lastLockTime]); - - const handleClearState = () => { - setSeedPhraseInfo([ - { - value: "", - suggestions: [], - }, - ]); - setLastFocusIndex(null); - setAlertIsOpen(false); - seedPhraseRef.current?.focusInputByIndex(0); - }; - - const renderSuggestSeedPhrase = (inputWord: string) => { - inputWord = inputWord.toLowerCase().trim(); - - if (!inputWord) { - return []; - } - - const suggestionWords: string[] = []; - for (let index = 0; index < SELECT_WORD_LIST.length; index++) { - const word = SELECT_WORD_LIST[index]; - - if (word.startsWith(inputWord)) { - suggestionWords.push(word); - } - - if (suggestionWords.length >= SUGGEST_SEED_PHRASE_LENGTH) { - break; - } - } - - return suggestionWords; - }; - - const handleUpdateSeedPhrase = (value: string, index: number) => { - const currentValue = [...seedPhraseInfo]; - - const suggestions = renderSuggestSeedPhrase(value); - - currentValue[index] = { - value, - suggestions: suggestions, - }; - - if (!value && index === currentValue.length - 2) { - currentValue.splice(currentValue.length - 2, 1); - } else if ( - index + 1 === currentValue.length && - index + 1 < SEED_PHRASE_LENGTH - ) { - currentValue.push({ - value: "", - suggestions: [], - }); - } - - setSeedPhraseInfo(currentValue); - - return currentValue; - }; - - const addSeedPhraseSelected = (word: string) => { - if (lastFocusIndex === null) return; - - const newValue = handleUpdateSeedPhrase(word, lastFocusIndex); - - if ( - lastFocusIndex !== SEED_PHRASE_LENGTH - 1 && - filledSeedPhrase.length !== SEED_PHRASE_LENGTH - ) { - seedPhraseRef.current?.focusInputByIndex(newValue.length - 1); - } - }; - - const handleContinue = async () => { - try { - const seedPhraseText = seedPhrase.join(" "); - const verifyResult = await Agent.agent.isMnemonicValid(seedPhraseText); - - if (!verifyResult) { - throw new Error(INVALID_SEED_PHRASE); - } - - dispatch( - setSeedPhraseCache({ - seedPhrase: seedPhraseText, - bran: "", - }) - ); - SecureStorage.delete(KeyStoreKeys.RECOVERY_WALLET_LAST_FAIL_ATTEMPT_TIME); - setFailAttempt(0); - - handleNavigate(); - } catch (e) { - setFailAttempt((value) => value + 1); - - if (failAttempt + 1 >= MAX_ATTEMPT_FAIL) { - const now = new Date(); - SecureStorage.set( - KeyStoreKeys.RECOVERY_WALLET_LAST_FAIL_ATTEMPT_TIME, - String(now.getTime()) - ); - setLastLockTime(now); - setAlertManyAttempOpen(true); - setLockFailAttempt(true); - } else { - setAlertIsOpen(true); - } - } - }; - - const handleNavigate = () => { - const data: DataProps = { - store: { stateCache }, - state: { - currentOperation: stateCache.currentOperation, - }, - }; - - const { nextPath, updateRedux } = getNextRoute( - RoutePath.VERIFY_RECOVERY_SEED_PHRASE, - data - ); - - updateReduxState(nextPath.pathname, data, dispatch, updateRedux); - handleClearState(); - - ionRouter.push(nextPath.pathname, "root", "replace"); - }; - - const handleBack = () => { - handleClearState(); - const { backPath, updateRedux } = getBackRoute( - RoutePath.VERIFY_RECOVERY_SEED_PHRASE, - { - store: { stateCache }, - } - ); - updateReduxState( - backPath.pathname, - { store: { stateCache } }, - dispatch, - updateRedux - ); - history.push({ - pathname: backPath.pathname, - }); - }; - - const closeFailAlert = () => { - setAlertIsOpen(false); - }; - - const closeClearAlert = () => { - setClearAlertOpen(false); - }; - - const onFocusInput = (index: number) => { - setLastFocusIndex(index); - setIsTyping(true); - const word = seedPhrase[index]; - renderSuggestSeedPhrase(word); - }; - - const checkMatchWithSuggestion = (selectWord: string | undefined) => { - return SELECT_WORD_LIST.some( - (word) => selectWord?.toLowerCase().trim() === word - ); - }; - - const onBlurInput = (index: number) => { - setIsTyping(false); - }; - - const verifyButtonLabel = `${i18n.t( - lockFailAttempt - ? "verifyrecoveryseedphrase.button.lock" - : "verifyrecoveryseedphrase.button.continue" - )}`; - - const displaySuggestionError = errorInputIndex.length > 0; - const filledSeedPhrase = seedPhrase.filter((item) => !!item); - const isMatchAllSuggestion = filledSeedPhrase.every((seedPhrase) => - checkMatchWithSuggestion(seedPhrase) - ); - - return ( - <> - { - handleClearState(); - handleBack(); - }} - currentPath={RoutePath.VERIFY_RECOVERY_SEED_PHRASE} - progressBar={true} - progressBarValue={0.75} - progressBarBuffer={1} - /> - } - > -
-
-

- {i18n.t("verifyrecoveryseedphrase.title")} -

-

- {i18n.t("verifyrecoveryseedphrase.paragraph.top")} -

- - {(suggestSeedPhrase.length > 0 || displaySuggestionError) && ( -

- {i18n.t("verifyrecoveryseedphrase.suggestions.title")} -

- )} - {suggestSeedPhrase.length > 0 && ( - - )} - {displaySuggestionError && ( -

- {i18n.t("verifyrecoveryseedphrase.suggestions.error")} -

- )} - {seedPhrase.filter((item) => !!item).length > 0 && ( - setClearAlertOpen(true)} - fill="outline" - data-testid="verify-clear-button" - className="clear-button secondary-button" - > - - {i18n.t("verifyrecoveryseedphrase.button.clear")} - - )} -
- handleContinue()} - primaryButtonDisabled={ - filledSeedPhrase.length < SEED_PHRASE_LENGTH || - lockFailAttempt || - displaySuggestionError || - !isMatchAllSuggestion - } - /> -
-
- - - - - ); -}; - -export { VerifyRecoverySeedPhrase }; diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.types.ts b/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.types.ts deleted file mode 100644 index 620609c97..000000000 --- a/src/ui/pages/VerifyRecoverySeedPhrase/VerifyRecoverySeedPhrase.types.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface SeedPhraseInfo { - value: string; - suggestions: string[]; -} - -export type { SeedPhraseInfo }; diff --git a/src/ui/pages/VerifyRecoverySeedPhrase/index.ts b/src/ui/pages/VerifyRecoverySeedPhrase/index.ts deleted file mode 100644 index 4e21faa7f..000000000 --- a/src/ui/pages/VerifyRecoverySeedPhrase/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./VerifyRecoverySeedPhrase"; diff --git a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx index 69641e8bf..5d9a293f9 100644 --- a/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx +++ b/src/ui/pages/VerifySeedPhrase/VerifySeedPhrase.test.tsx @@ -298,7 +298,7 @@ describe("Verify Seed Phrase Page", () => { fireEvent.click(backButton); }); - expect(dispatchMock).toBeCalledTimes(2); + expect(continueButton.disabled).toBe(true); }); test("The user can remove words from the Seed Phrase", async () => { From de18f8fef373e19fb564cf525c3b7bb6240d88b9 Mon Sep 17 00:00:00 2001 From: Fergal Date: Mon, 17 Jun 2024 10:40:50 +0100 Subject: [PATCH 28/28] ci(gha): stop docker builds on every push (#526) --- .github/workflows/docker-builds.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/docker-builds.yaml b/.github/workflows/docker-builds.yaml index 5c3d32917..3399daa34 100644 --- a/.github/workflows/docker-builds.yaml +++ b/.github/workflows/docker-builds.yaml @@ -1,12 +1,6 @@ name: Build and publish docker artifacts on: - push: - branches: - - main - - develop - pull_request: - types: [ opened, synchronize ] workflow_dispatch: env: